From 0d3d9727c6f0efe691cee2a0fe7e2c3e4f00baee Mon Sep 17 00:00:00 2001 From: chinboy Date: Mon, 17 Aug 2026 23:07:14 +0800 Subject: [PATCH] fix(AOT): restore sidecar delivery and narrow linked fallbacks --- build/AtomUI.LinkedRegistration.targets | 32 ++++++++++- .../BrowserGalleryApplication.cs | 2 + .../GalleryApplication.cs | 4 +- .../scripts/PublishToLocal.ps1 | 2 + .../ThemeManagerBuilderExtensions.cs | 7 ++- .../foundations/aot-and-trimming.md | 5 +- .../aot-linked-registration-pipeline.md | 9 ++- .../development/aot-programming-guidelines.md | 11 +++- docs/modules/generator/linked-registration.md | 4 +- .../aot/linked-registration-sidecar.md | 11 +++- .../GenerateLinkedRegistrationSidecarTask.cs | 21 +++++++ src/AtomUI.Core/AppBuilderExtensions.cs | 25 ++++++--- src/AtomUI.Core/AtomUI.Core.csproj | 3 +- ...kedRegistrationPackageManifestGenerator.cs | 9 ++- .../AtomUIDiagnosticDescriptors.cs | 9 +++ .../Diagnostics/AtomUIDiagnosticIds.cs | 1 + .../ApplicationRegistrationPlanGenerator.cs | 36 ++++++++++-- .../LinkedRegistrationProtocol.cs | 9 +++ .../LinkedRegistrationUsageGenerator.cs | 15 ++--- ...plicationRegistrationPlanGeneratorTests.cs | 56 +++++++++++++++++++ .../LinkedRegistrationBuildAssetsTests.cs | 48 ++++++++++++++++ ...nkedRegistrationDiagnosticContractTests.cs | 11 +++- .../LinkedRegistrationUsageGeneratorTests.cs | 22 +++++++- .../ThemeSchemaGeneratorTests.cs | 8 +-- 24 files changed, 314 insertions(+), 46 deletions(-) diff --git a/build/AtomUI.LinkedRegistration.targets b/build/AtomUI.LinkedRegistration.targets index 18d634551..5b1d3efde 100644 --- a/build/AtomUI.LinkedRegistration.targets +++ b/build/AtomUI.LinkedRegistration.targets @@ -49,6 +49,36 @@ AtomUILinkedSidecar="true" Visible="false" /> + + + <_AtomUILinkedExtractableReference + Include="@(ReferencePath)" + Condition="'%(ReferencePath.FileName)' != '' and + $([System.String]::Copy('%(ReferencePath.FileName)').StartsWith('AtomUI.', System.StringComparison.Ordinal)) and + !Exists('%(ReferencePath.FullPath).atomui-link.json')" /> + + + + + + + + + RemoveProperties="PublishAot;PublishTrimmed;RunAOTCompilation;NoBuild"> diff --git a/controlgallery/AtomUIGallery.Browser/BrowserGalleryApplication.cs b/controlgallery/AtomUIGallery.Browser/BrowserGalleryApplication.cs index f66e9b47d..9e5189835 100644 --- a/controlgallery/AtomUIGallery.Browser/BrowserGalleryApplication.cs +++ b/controlgallery/AtomUIGallery.Browser/BrowserGalleryApplication.cs @@ -4,6 +4,7 @@ using AtomUI.Desktop.Controls; using AtomUI.Fonts.AlibabaPuHuiTi; using AtomUI.Localization; using AtomUI.Theme; +using AtomUI.Toolkits.GalleryBase; using AtomUIGallery.Localization; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; @@ -29,6 +30,7 @@ public partial class BrowserGalleryApplication : Application builder.UseDesktopExtras(); builder.UseDesktopColorPicker(); builder.UseDesktopDataGrid(); + builder.UseGalleryBase(AtomUIGalleryModule.Configure); builder.UseGalleryControls(); }); } diff --git a/controlgallery/AtomUIGallery.Desktop/GalleryApplication.cs b/controlgallery/AtomUIGallery.Desktop/GalleryApplication.cs index b44100f4f..6cdab25fd 100644 --- a/controlgallery/AtomUIGallery.Desktop/GalleryApplication.cs +++ b/controlgallery/AtomUIGallery.Desktop/GalleryApplication.cs @@ -1,8 +1,9 @@ -using System.Globalization; +using System.Globalization; using AtomUI; using AtomUI.Desktop.Controls; using AtomUI.Localization; using AtomUI.Theme; +using AtomUI.Toolkits.GalleryBase; using AtomUIGallery.Localization; using Avalonia.Controls.ApplicationLifetimes; @@ -31,6 +32,7 @@ public partial class GalleryApplication : BaseGalleryApplication builder.UseDesktopExtras(); builder.UseDesktopColorPicker(); builder.UseDesktopDataGrid(); + builder.UseGalleryBase(AtomUIGalleryModule.Configure); builder.UseGalleryControls(); }); } diff --git a/controlgallery/AtomUIGallery.Desktop/scripts/PublishToLocal.ps1 b/controlgallery/AtomUIGallery.Desktop/scripts/PublishToLocal.ps1 index 2c1e77623..e6b9d74c1 100644 --- a/controlgallery/AtomUIGallery.Desktop/scripts/PublishToLocal.ps1 +++ b/controlgallery/AtomUIGallery.Desktop/scripts/PublishToLocal.ps1 @@ -252,6 +252,7 @@ if ($publishAotEnabled) { "--no-restore", "-p:GalleryPublishTrimmed=true", "-p:GalleryPublishAot=true", + "-p:AtomUILinkedPublish=true", "--disable-build-servers", "-m:1", "/nr:false", @@ -275,6 +276,7 @@ if ($publishAotEnabled) { $buildType, "-p:GalleryPublishTrimmed=true", "-p:GalleryPublishAot=false", + "-p:AtomUILinkedPublish=true", "-p:PublishSingleFile=true" ) } diff --git a/controlgallery/AtomUIGallery/ThemeManagerBuilderExtensions.cs b/controlgallery/AtomUIGallery/ThemeManagerBuilderExtensions.cs index f91e7cf8f..b0189740e 100644 --- a/controlgallery/AtomUIGallery/ThemeManagerBuilderExtensions.cs +++ b/controlgallery/AtomUIGallery/ThemeManagerBuilderExtensions.cs @@ -2,17 +2,20 @@ using AtomUI; using AtomUI.Generated.AtomUIGallery; using AtomUI.Theme; using AtomUI.Theme.Definitions; -using AtomUI.Toolkits.GalleryBase; namespace AtomUIGallery; public static class ThemeManagerBuilderExtensions { + /// + /// Registers Gallery-level services shared by all hosts. The GalleryBase control package + /// entry (UseGalleryBase) must be invoked directly by the application project so the + /// linked-registration plan can see it. + /// public static IAtomUIBuilder UseGalleryControls(this IAtomUIBuilder builder) { ArgumentNullException.ThrowIfNull(builder); GeneratedLanguageModuleRegistration.Register(builder.Localization); - builder.UseGalleryBase(AtomUIGalleryModule.Configure); builder.Theme.AddThemeDefinitionResolver( new AvaloniaAssetThemeDefinitionResolver( "AtomUIGallery.BuiltInThemes", diff --git a/docs/architecture/foundations/aot-and-trimming.md b/docs/architecture/foundations/aot-and-trimming.md index 076cf1278..5f0f6f6e2 100644 --- a/docs/architecture/foundations/aot-and-trimming.md +++ b/docs/architecture/foundations/aot-and-trimming.md @@ -156,8 +156,10 @@ dynamic、reflection、Loose AXAML 和插件输入触发 Package fallback。 | 静态 Control/AXAML 可确定 | 选择对应 Unit closure | | 默认 Package 粒度 | 选择完整 Package Unit | | Unit cycle | SCC 全选,每个 leaf fragment 调用一次 | -| dynamic/reflection/Loose AXAML | 对应 Package full registrar | +| Loose AXAML / 动态主题 | 对应 Package full registrar | +| 无法静态解析的 C# 动态创建(`Activator.CreateInstance(Type)` 等) | 不扩大保留范围,报告 `ATOMUILINK010` 警告,由显式 root 覆盖 | | Sidecar 缺失、陈旧或无法验证 | 对应 Package full registrar | +| ProjectReference Sidecar 由 consumer 从普通构建的 assembly metadata 提取(`ExtractedManifest`) | 对应 Package full registrar,不产生诊断 | | 分析预算超限 | 对应 Package full registrar | | 未知 protocol major | 构建 Error | | Fragment symbol 不存在 | 构建 Error | @@ -236,6 +238,7 @@ ordinary Generator、linked-publish Generator、Build Tasks、Sidecar、PDB 和 | `ATOMUILINK007` | Loose AXAML 或动态主题导致 Package fallback | Warning | | `ATOMUILINK008` | 检测到 Package 使用但缺少 registration entry | Error | | `ATOMUILINK009` | Registration entry Attribute 或签名无效 | Error | +| `ATOMUILINK010` | C# 动态创建无法静态解析,不扩大保留范围,需显式 root 覆盖 | Warning | 诊断必须包含 Package/Unit identity、稳定 reason 和可定位输入,不能只写“可能不兼容 AOT”。Fallback Warning 只在 linked publish 或显式 strict 验证中产生;strict 模式可以提升为 Error,但不改变保留范围。 diff --git a/docs/architecture/foundations/aot-linked-registration-pipeline.md b/docs/architecture/foundations/aot-linked-registration-pipeline.md index 98cffb450..c18597055 100644 --- a/docs/architecture/foundations/aot-linked-registration-pipeline.md +++ b/docs/architecture/foundations/aot-linked-registration-pipeline.md @@ -110,6 +110,12 @@ Sidecar 是纯编译资产: - Package 自己的 `.targets` 只在 linked build 中把它加入 `AdditionalFiles`。 - linked ProjectReference 在程序集复制到 `TargetPath` 后生成 `.atomui-link.json`;消费项目从已解析 `ReferencePath` 的 companion 路径收集它,不递归调用 ProjectReference target。 +- companion Sidecar 缺失时(引用库是普通构建,例如 `PublishAot`/`PublishTrimmed` 只在应用项目局部设置),消费项目直接从 + 引用 assembly 的 metadata 记录提取 Sidecar 到自身 `obj`。普通库构建始终包含 Package/Unit/ControlMap/Axaml UnitEdge + 记录,但 C# UnitEdge 只由 linked 库构建计算,所以提取的 Sidecar 对每个 Package 附加 `ExtractedManifest` fallback: + Application Plan 对该 Package 保持 full fallback,且不产生诊断。publish 时以全局属性传入 + `-p:PublishAot=true` / `-p:PublishTrimmed=true` / `-p:AtomUILinkedPublish=true`,引用库即自行产出完整 Sidecar, + 提取路径自动旁路。 - `CopyToOutputDirectory`、`CopyToPublishDirectory` 均为 `Never`。 - 不作为 EmbeddedResource,不进入运行时程序集、应用输出或 publish 目录。 @@ -259,7 +265,8 @@ Sidecar 不进入运行时。`UseXxxControls()` 仍拥有 Package Core、Provide | --- | --- | | 精确静态 usage | 选择 Unit closure | | 默认 Package 粒度 | 选择单一完整 Package Unit | -| dynamic/reflection/loose AXAML | 当前 Package full registrar | +| loose AXAML / 动态主题 | 当前 Package full registrar | +| 无法静态解析的 C# 动态创建 | 不扩大保留范围,报告 `ATOMUILINK010` 警告,由显式 root 覆盖 | | sidecar 缺失、陈旧或无法绑定 | 当前相关 Package full registrar | | 未知 protocol major | 构建 Error | | fragment symbol 不存在 | 构建 Error | diff --git a/docs/engineering/development/aot-programming-guidelines.md b/docs/engineering/development/aot-programming-guidelines.md index 26d33412b..f49d4b7dd 100644 --- a/docs/engineering/development/aot-programming-guidelines.md +++ b/docs/engineering/development/aot-programming-guidelines.md @@ -292,9 +292,14 @@ Package、Unit、ControlMap 或 full fragment metadata。 Attribute,不要为了让 Common 参与应用计划而复制 `UseDesktopControls()` 的方法 identity,也不要引入跨 Package Unit 闭包。 类库只在被 linked 应用作为 ProjectReference 构建或执行 NuGet Pack 时生成 Usage Sidecar。普通 Debug/Release 必须跳过 usage -分析和 Sidecar 生成。缺失或无法验证的 Sidecar 不能解释为没有 usage,只能让相关 Package full fallback。 -`ATOMUILINK002` 和 `ATOMUILINK007` 只在 linked publish 或显式 `AtomUIRegistrationStrict=true` 验证中显示;strict 模式用于 -CI 把自动 full fallback 提升为 error。 +分析和 Sidecar 生成。应用侧编译时如果 ProjectReference 旁路没有 Sidecar,consumer 直接从引用 assembly 的 metadata 记录提取 +(普通库构建始终包含 Package/Unit/ControlMap/Axaml UnitEdge 记录),用于诊断与入口校验;提取的 Sidecar 带 +`ExtractedManifest` fallback 标记,相关 Package 在 Application Plan 中保持 full fallback,不产生诊断。要获得 Unit 级裁剪, +publish 时以全局属性传入 `-p:PublishAot=true` / `-p:PublishTrimmed=true`(命令行全局属性沿 ProjectReference 流动,引用库 +自行启用 linked 构建并产出含 C# UnitEdge 的完整 Sidecar);如果应用在项目文件里局部设置这些属性,则 publish 时补传 +`-p:AtomUILinkedPublish=true`。缺失或无法验证的 Sidecar 不能解释为没有 usage,只能让相关 Package full fallback。 +`ATOMUILINK002`、`ATOMUILINK007` 和 `ATOMUILINK010` 只在 linked publish 或显式 `AtomUIRegistrationStrict=true` 验证中显示;strict 模式用于 +CI 把自动 full fallback 或未覆盖的动态创建提升为 error。`ATOMUILINK010` 不触发 full fallback,只提示用显式 root 覆盖。 修改 Generator ABI、Sidecar schema、feature switch 或 Public fragment entry point 时,按 Public API 和协议 review,并运行 普通构建零 linked-analysis、trimmed JIT、NativeAOT 和非裁剪兼容验证。系统契约见 diff --git a/docs/modules/generator/linked-registration.md b/docs/modules/generator/linked-registration.md index 349b1b1fe..6a9de41b8 100644 --- a/docs/modules/generator/linked-registration.md +++ b/docs/modules/generator/linked-registration.md @@ -161,7 +161,7 @@ writer 不生成 `AddDependencies`,不调用其他 Unit,也不调用 `TryEnt 诊断必须使用稳定 reason code,并包含 Package 和可用 source identity。至少区分: - unresolved owner。 -- dynamic/reflection usage。 +- dynamic/reflection usage(不扩大保留范围,报告 `ATOMUILINK010`,由显式 root 覆盖)。 - loose or unresolved AXAML。 - missing/stale sidecar。 - unknown protocol major。 @@ -214,7 +214,7 @@ Package 与 entry 信息。SCC 发现和组件依赖排序必须使用显式栈 - Debug/普通 Release 的 `Csc` Analyzer item 不含 linked analyzer。 - linked AXAML target 在普通构建 skipped。 -- ProjectReference sidecar 只在 linked build 传递。 +- ProjectReference sidecar 只在 linked build 传递;companion 缺失时 consumer 从引用 assembly metadata 提取,提取结果带 `ExtractedManifest` fallback(full fallback、无诊断)。 - NuGet sidecar/consumer target 自动打包且不进入 runtime assets。 - sidecar 缺失、损坏或无法验证时只触发 Package fallback。 diff --git a/docs/reference/aot/linked-registration-sidecar.md b/docs/reference/aot/linked-registration-sidecar.md index fca7ef2ca..f96267072 100644 --- a/docs/reference/aot/linked-registration-sidecar.md +++ b/docs/reference/aot/linked-registration-sidecar.md @@ -9,6 +9,7 @@ - 内容:单个 JSON object。 - NuGet 位置:`buildTransitive/AtomUI.LinkedRegistration/`。 - ProjectReference 位置:项目 `obj` 输出,由 MSBuild target item 传递。 +- ProjectReference 旁路缺失时,consumer 直接从引用 assembly 的 metadata 记录提取 Sidecar 到自身 `obj`(见 `ExtractedManifest`)。 - 不作为 `EmbeddedResource`、`Content`、runtime asset 或 publish asset。 Producer 必须使用结构化 JSON writer。Consumer 必须使用结构化 parser;不得用正则或字符串切割解析。 @@ -124,8 +125,16 @@ Fallback record 包含: - `MissingSidecar` - `StaleSidecar` - `AnalysisBudgetExceeded` +- `ExtractedManifest` -Fallback 只能扩大保留范围。Consumer 不得忽略未知必需 reason,也不得把 fallback 降级为 Exact。 +Fallback 只能扩大保留范围。Consumer 不得忽略未知必需 reason,也不得把 fallback 降级为 Exact。例外有两个: + +- `DynamicInvocation`:提示性记录,Consumer 不扩大保留范围,只报告 `ATOMUILINK010` 警告,由显式 + `AtomUIRegistrationUnitRoot` / `AtomUIPackageRoot` 覆盖。 +- `ExtractedManifest`:consumer 从普通构建的 ProjectReference assembly metadata 提取的 Sidecar 标记。 + Package/Unit 清单完整,但 C# UnitEdge 只由 linked 库构建计算,所以 Consumer 必须对相关 Package 扩大为 + full fallback,且不产生任何诊断。要获得完整裁剪,发布时以全局属性传入 `-p:PublishAot=true` / + `-p:PublishTrimmed=true` / `-p:AtomUILinkedPublish=true`,让引用库自行产出完整 Sidecar。 ## 确定性 diff --git a/src/AtomUI.Build.Tasks/GenerateLinkedRegistrationSidecarTask.cs b/src/AtomUI.Build.Tasks/GenerateLinkedRegistrationSidecarTask.cs index 7ad3fc2a2..219e327a7 100644 --- a/src/AtomUI.Build.Tasks/GenerateLinkedRegistrationSidecarTask.cs +++ b/src/AtomUI.Build.Tasks/GenerateLinkedRegistrationSidecarTask.cs @@ -21,6 +21,14 @@ public sealed class GenerateLinkedRegistrationSidecarTask : ITask public string TargetFramework { get; set; } = string.Empty; + /// + /// When true, the Sidecar is extracted by a consumer from a normally-built referenced + /// assembly. C# unit edges are only emitted by linked library builds, so each declared + /// package gets an ExtractedManifest fallback marker: the plan widens to full fallback + /// instead of trimming with incomplete evidence. + /// + public bool ExtractedFallback { get; set; } + [Output] public string SidecarPath { get; private set; } = string.Empty; @@ -67,6 +75,19 @@ public sealed class GenerateLinkedRegistrationSidecarTask : ITask return LogError(error); } sidecar.Assembly.TargetFramework = TargetFramework.Trim(); + if (ExtractedFallback && sidecar.Packages.Length != 0) + { + sidecar.Fallbacks = sidecar.Fallbacks + .Concat(sidecar.Packages.Select(static package => new LinkedSidecarFallback + { + PackageId = package.Id, + Reason = LinkedRegistrationProtocol.FallbackReasonExtractedManifest, + Source = "", + Line = 0, + Column = 0 + })) + .ToArray(); + } var bytes = LinkedRegistrationSidecarCodec.Write(sidecar); if (analysisBudgetExceeded || bytes.Length > LinkedRegistrationAnalysisBudget.MaxSidecarBytes) diff --git a/src/AtomUI.Core/AppBuilderExtensions.cs b/src/AtomUI.Core/AppBuilderExtensions.cs index d4191b85a..d80f16ac7 100644 --- a/src/AtomUI.Core/AppBuilderExtensions.cs +++ b/src/AtomUI.Core/AppBuilderExtensions.cs @@ -48,8 +48,11 @@ public static class AppBuilderExtensions /// public static AppBuilder WithAtomUIDefaultOptions(this AppBuilder appBuilder) { - return appBuilder - .With(new AvaloniaNativePlatformOptions + // 按目标平台分支设置平台选项。NativeAOT 在 RID 发布时会把 OperatingSystem.IsXxx() + // 折叠为常量,非目标平台的分支及其引用的平台选项类型会被整体裁剪。 + if (OperatingSystem.IsMacOS()) + { + appBuilder = appBuilder.With(new AvaloniaNativePlatformOptions { RenderingMode = [ @@ -57,8 +60,11 @@ public static class AppBuilderExtensions AvaloniaNativeRenderingMode.Metal, AvaloniaNativeRenderingMode.Software ] - }) - .With(new Win32PlatformOptions + }); + } + if (OperatingSystem.IsWindows()) + { + appBuilder = appBuilder.With(new Win32PlatformOptions { RenderingMode = [ @@ -66,11 +72,16 @@ public static class AppBuilderExtensions Win32RenderingMode.Software ], CompositionMode = [Win32CompositionMode.RedirectionSurface] - }) - .With(new X11PlatformOptions + }); + } + if (OperatingSystem.IsLinux()) + { + appBuilder = appBuilder.With(new X11PlatformOptions { EnableDrawnDecorations = true - }) + }); + } + return appBuilder .With(new FontManagerOptions { FontFallbacks = [new FontFallback diff --git a/src/AtomUI.Core/AtomUI.Core.csproj b/src/AtomUI.Core/AtomUI.Core.csproj index f56d3eb14..b2f2d23ea 100644 --- a/src/AtomUI.Core/AtomUI.Core.csproj +++ b/src/AtomUI.Core/AtomUI.Core.csproj @@ -1,4 +1,4 @@ - + $(AtomUITargetFrameworks) AtomUI @@ -26,7 +26,6 @@ - diff --git a/src/AtomUI.Generator.LinkedPublish/LinkedRegistrationPackageManifestGenerator.cs b/src/AtomUI.Generator.LinkedPublish/LinkedRegistrationPackageManifestGenerator.cs index 6b4aedd92..53b5ca011 100644 --- a/src/AtomUI.Generator.LinkedPublish/LinkedRegistrationPackageManifestGenerator.cs +++ b/src/AtomUI.Generator.LinkedPublish/LinkedRegistrationPackageManifestGenerator.cs @@ -86,7 +86,7 @@ public sealed class LinkedRegistrationPackageManifestGenerator : IIncrementalGen { fallbacks.Add(new Fallback( packageId, - "DynamicInvocation", + LinkedRegistrationProtocol.FallbackReasonDynamicInvocation, NormalizeSourcePath(candidate.Source, projectDirectory), candidate.Line, candidate.Column, @@ -376,7 +376,12 @@ public sealed class LinkedRegistrationPackageManifestGenerator : IIncrementalGen AnalyzerConfigOptionsProvider optionsProvider, Fallback fallback) { - var descriptor = AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened; + var descriptor = string.Equals( + fallback.Reason, + LinkedRegistrationProtocol.FallbackReasonDynamicInvocation, + StringComparison.Ordinal) + ? AtomUIDiagnosticDescriptors.LinkedDynamicUsageUncovered + : AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened; if (LinkedRegistrationOptions.IsRegistrationStrict(optionsProvider)) { descriptor = new DiagnosticDescriptor( diff --git a/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticDescriptors.cs b/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticDescriptors.cs index f9a090fb2..00a8c4eea 100644 --- a/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticDescriptors.cs +++ b/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticDescriptors.cs @@ -86,6 +86,15 @@ internal static class AtomUIDiagnosticDescriptors isEnabledByDefault: true, customTags: [WellKnownDiagnosticTags.Telemetry]); + public static readonly DiagnosticDescriptor LinkedDynamicUsageUncovered = new( + AtomUIDiagnosticIds.LinkedDynamicUsageUncovered, + "Dynamic AtomUI usage is not covered by the static registration plan", + "AtomUI dynamic usage '{0}' cannot be resolved statically; controls of Package '{1}' created only through this site are not registered. Add an AtomUIRegistrationUnitRoot when the Unit is known, or an AtomUIPackageRoot for fully dynamic usage; no action is required when this site never creates AtomUI controls.", + AtomUIDiagnosticCategories.LinkedRegistration, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + customTags: [WellKnownDiagnosticTags.Telemetry]); + public static readonly DiagnosticDescriptor AotMissingGeneratedAccessor = new( AtomUIDiagnosticIds.AotMissingGeneratedAccessor, "AOT-sensitive data member path requires generated accessor", diff --git a/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticIds.cs b/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticIds.cs index a7854e98a..88d2b8aa9 100644 --- a/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticIds.cs +++ b/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticIds.cs @@ -40,4 +40,5 @@ internal static class AtomUIDiagnosticIds public const string LinkedLooseAxamlWidened = "ATOMUILINK007"; public const string LinkedPackageEntryMissing = "ATOMUILINK008"; public const string LinkedPackageEntryInvalid = "ATOMUILINK009"; + public const string LinkedDynamicUsageUncovered = "ATOMUILINK010"; } diff --git a/src/AtomUI.Generator/LinkedRegistration/ApplicationRegistrationPlanGenerator.cs b/src/AtomUI.Generator/LinkedRegistration/ApplicationRegistrationPlanGenerator.cs index 902771b4c..f80f96e0c 100644 --- a/src/AtomUI.Generator/LinkedRegistration/ApplicationRegistrationPlanGenerator.cs +++ b/src/AtomUI.Generator/LinkedRegistration/ApplicationRegistrationPlanGenerator.cs @@ -117,8 +117,16 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator usage.Kind == LinkedUsageKind.PackageRoot && string.Equals(usage.Identity, packageId, StringComparison.Ordinal)) || catalog.Fallbacks.Any(fallback => - fallback.PackageId.Length == 0 || - string.Equals(fallback.PackageId, packageId, StringComparison.Ordinal)); + // Unresolvable C# dynamic creation (DynamicInvocation) is reported as + // guidance (ATOMUILINK010) but never widens the plan: application-declared + // controls are already covered by base-type evidence, and purely + // string-driven creation is covered by explicit Unit/Package roots. + !string.Equals( + fallback.Reason, + LinkedRegistrationProtocol.FallbackReasonDynamicInvocation, + StringComparison.Ordinal) && + (fallback.PackageId.Length == 0 || + string.Equals(fallback.PackageId, packageId, StringComparison.Ordinal))); if (isLegacy) { if (ValidateFragmentMethod( @@ -231,12 +239,29 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator .ThenBy(static item => item.Column) .ThenBy(static item => item.Reason, StringComparer.Ordinal)) { + // ExtractedManifest marks a consumer-extracted ProjectReference Sidecar: the plan + // already widened to full fallback, and the delivery mode is not a code problem. + if (string.Equals( + fallback.Reason, + LinkedRegistrationProtocol.FallbackReasonExtractedManifest, + StringComparison.Ordinal)) + { + continue; + } if (fallback.PackageId.Length != 0 && !invokedPackageSet.Contains(fallback.PackageId)) { continue; } + var baseDescriptor = string.Equals( + fallback.Reason, + LinkedRegistrationProtocol.FallbackReasonDynamicInvocation, + StringComparison.Ordinal) + ? AtomUIDiagnosticDescriptors.LinkedDynamicUsageUncovered + : AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened; context.ReportDiagnostic(Diagnostic.Create( - GetFallbackDescriptor(LinkedRegistrationOptions.IsRegistrationStrict(optionsProvider)), + GetFallbackDescriptor( + LinkedRegistrationOptions.IsRegistrationStrict(optionsProvider), + baseDescriptor), Location.None, fallback.Reason, fallback.PackageId.Length == 0 ? "" : fallback.PackageId)); @@ -379,13 +404,12 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator descriptor.CustomTags.ToArray()); } - private static DiagnosticDescriptor GetFallbackDescriptor(bool strict) + private static DiagnosticDescriptor GetFallbackDescriptor(bool strict, DiagnosticDescriptor descriptor) { if (!strict) { - return AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened; + return descriptor; } - var descriptor = AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened; return new DiagnosticDescriptor( descriptor.Id, descriptor.Title, diff --git a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationProtocol.cs b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationProtocol.cs index 82c5db5d5..572d8b5f4 100644 --- a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationProtocol.cs +++ b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationProtocol.cs @@ -12,4 +12,13 @@ internal static class LinkedRegistrationProtocol internal const string UsageManifestKey = MetadataPrefix + "Usage.v1"; internal const string FallbackManifestKey = MetadataPrefix + "Fallback.v1"; internal const string PlanMarkerKey = MetadataPrefix + "Plan.v1"; + + internal const string FallbackReasonAnalysisBudgetExceeded = "AnalysisBudgetExceeded"; + internal const string FallbackReasonDynamicInvocation = "DynamicInvocation"; + internal const string FallbackReasonUnresolvedOwner = "UnresolvedOwner"; + // Sidecar extracted by the consumer from assembly metadata of a normally-built + // ProjectReference: package/unit manifests are complete, but C# unit edges are only + // computed by linked library builds. The plan must widen to full fallback for these + // packages, and the reason is never surfaced as a diagnostic. + internal const string FallbackReasonExtractedManifest = "ExtractedManifest"; } diff --git a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationUsageGenerator.cs b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationUsageGenerator.cs index 87829e279..c3c4b7951 100644 --- a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationUsageGenerator.cs +++ b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationUsageGenerator.cs @@ -387,14 +387,15 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator static packageId => packageId, StringComparer.Ordinal)) { - AddPackageFallback( - packageId, - dynamicUse.Source, - dynamicUse.Line, - dynamicUse.Column, + // C# dynamic creation sites no longer widen the package: controls declared + // in the application are already covered by base-type evidence, and purely + // string-driven creation is covered by explicit Unit/Package roots. Report + // guidance only. + ReportFallback( + AtomUIDiagnosticDescriptors.LinkedDynamicUsageUncovered, dynamicUse.Location, - AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened, - dynamicUse.Identity); + dynamicUse.Identity, + packageId); } } diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/ApplicationRegistrationPlanGeneratorTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/ApplicationRegistrationPlanGeneratorTests.cs index ace8fa6e2..719918a9c 100644 --- a/tests/AtomUI.Generator.Tests/LinkedRegistration/ApplicationRegistrationPlanGeneratorTests.cs +++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/ApplicationRegistrationPlanGeneratorTests.cs @@ -170,6 +170,62 @@ public sealed class ApplicationRegistrationPlanGeneratorTests result.PlanSource.ShouldBeNull(); } + [Fact] + public void Dynamic_Invocation_Fallback_Warns_Without_Widening_The_Package() + { + var desktop = PlanGeneratorTestHost.StandardDesktopPackage(); + var usage = PlanGeneratorTestHost.Usage( + new LinkedUsageManifestRecord(LinkedUsageKind.Control, "AtomUI.Desktop.Controls.Button", "View.axaml", 3, 4)); + var dynamicFallback = UsageGeneratorTestHost.CreateManifestReference( + "Consumer.Usage.DynamicFallback", + new LinkedFallbackManifestRecord( + "AtomUI.Desktop.Controls", + LinkedRegistrationProtocol.FallbackReasonDynamicInvocation, + "ViewLocator.cs", + 27, + 29)); + + var result = PlanGeneratorTestHost.Run( + "AtomUI.Desktop.Entry.UseDesktopControls();", + [desktop, usage, dynamicFallback]); + var planSource = result.PlanSource; + planSource.ShouldNotBeNull(); + + planSource!.ShouldContain("global::GeneratedButtonUnitFragment.Add(packageBuilder);"); + planSource.ShouldNotContain("global::GeneratedDatePickerUnitFragment.Add(packageBuilder);"); + planSource.ShouldNotContain("global::GeneratedDesktopFullFragment.Register("); + result.Diagnostics.ShouldContain(static diagnostic => + diagnostic.Id == "ATOMUILINK010" && diagnostic.Severity == DiagnosticSeverity.Warning); + } + + [Fact] + public void Extracted_Manifest_Fallback_Widens_Without_A_Diagnostic() + { + var desktop = PlanGeneratorTestHost.StandardDesktopPackage(); + var usage = PlanGeneratorTestHost.Usage( + new LinkedUsageManifestRecord(LinkedUsageKind.Control, "AtomUI.Desktop.Controls.Button", "View.axaml", 3, 4)); + var extractedFallback = UsageGeneratorTestHost.CreateManifestReference( + "Consumer.ExtractedSidecar", + new LinkedFallbackManifestRecord( + "AtomUI.Desktop.Controls", + LinkedRegistrationProtocol.FallbackReasonExtractedManifest, + "", + 0, + 0)); + + var result = PlanGeneratorTestHost.Run( + "AtomUI.Desktop.Entry.UseDesktopControls();", + [desktop, usage, extractedFallback]); + var planSource = result.PlanSource.ShouldNotBeNull(); + + planSource.ShouldContain("global::GeneratedDesktopFullFragment.Register("); + planSource.ShouldNotContain("global::GeneratedButtonUnitFragment.Add(packageBuilder);"); + result.Diagnostics.ShouldNotContain(static diagnostic => + diagnostic.Id == "ATOMUILINK002" || + diagnostic.Id == "ATOMUILINK007" || + diagnostic.Id == "ATOMUILINK010"); + } + [Fact] public void Explicit_Unit_And_Package_Roots_Select_Precise_And_Full_Plans() { diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationBuildAssetsTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationBuildAssetsTests.cs index e784dbe48..efc1e0c11 100644 --- a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationBuildAssetsTests.cs +++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationBuildAssetsTests.cs @@ -252,6 +252,30 @@ public sealed class LinkedRegistrationBuildAssetsTests condition.ShouldContain("'$(AtomUIRegistrationPackageId)' != ''"); } + [Fact] + public void Pack_Sidecar_Inner_Build_Does_Not_Inherit_Publish_Or_NoBuild_Globals() + { + // dotnet pack --no-build exports NoBuild=true as a global property; if the inner + // MSBuild call that emits the linked sidecar inherits it, the SDK fails the pack + // with NETSDK1085 and registration packages silently lose their sidecar assets. + var targets = XDocument.Load(GetRepoFile("build/AtomUI.LinkedRegistration.targets")); + var target = targets.Descendants("Target") + .Single(element => + (string?)element.Attribute("Name") == + "AtomUIPrepareLinkedRegistrationSidecarForPack"); + + var removeProperties = (string?)target.Descendants("MSBuild") + .Single() + .Attribute("RemoveProperties"); + removeProperties.ShouldNotBeNull(); + var removed = removeProperties.Split(';', StringSplitOptions.RemoveEmptyEntries | + StringSplitOptions.TrimEntries); + removed.ShouldContain("NoBuild"); + removed.ShouldContain("PublishAot"); + removed.ShouldContain("PublishTrimmed"); + removed.ShouldContain("RunAOTCompilation"); + } + [Fact] public void ProjectReference_Sidecars_Are_Collected_From_Resolved_Assembly_Paths() { @@ -289,6 +313,30 @@ public sealed class LinkedRegistrationBuildAssetsTests .ShouldBe("Build;GetAtomUILinkedRegistrationSidecar"); } + [Fact] + public void Missing_ProjectReference_Sidecars_Are_Extracted_With_A_Fallback_Marker() + { + var targets = XDocument.Load(GetRepoFile("build/AtomUI.LinkedRegistration.targets")); + var collectTarget = targets.Descendants("Target") + .Single(element => + (string?)element.Attribute("Name") == + "CollectAtomUIProjectReferenceSidecars"); + + var extractable = collectTarget.Descendants("_AtomUILinkedExtractableReference") + .ShouldHaveSingleItem(); + ((string?)extractable.Attribute("Include")).ShouldBe("@(ReferencePath)"); + ((string?)extractable.Attribute("Condition")).ShouldNotBeNull() + .ShouldContain("StartsWith('AtomUI.'"); + + var extraction = collectTarget.Descendants("AtomUI.Build.Tasks.GenerateLinkedRegistrationSidecarTask") + .ShouldHaveSingleItem(); + ((string?)extraction.Attribute("ExtractedFallback")).ShouldBe("true"); + ((string?)extraction.Attribute("ContinueOnError")).ShouldBe("WarnAndContinue"); + var outputPath = (string?)extraction.Attribute("OutputPath"); + outputPath.ShouldNotBeNull(); + outputPath.ShouldContain("AtomUIExtractedSidecars"); + } + [Fact] public void Sidecars_Are_Emitted_After_The_Target_Assembly_Reaches_TargetPath() { diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationDiagnosticContractTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationDiagnosticContractTests.cs index fde73f3b6..a02e53c1b 100644 --- a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationDiagnosticContractTests.cs +++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationDiagnosticContractTests.cs @@ -22,9 +22,10 @@ public sealed class LinkedRegistrationDiagnosticContractTests "ATOMUILINK006", "ATOMUILINK007", "ATOMUILINK008", - "ATOMUILINK009" + "ATOMUILINK009", + "ATOMUILINK010" ]); - descriptors.Select(static descriptor => descriptor.Id).Distinct().Count().ShouldBe(9); + descriptors.Select(static descriptor => descriptor.Id).Distinct().Count().ShouldBe(10); } [Fact] @@ -41,6 +42,7 @@ public sealed class LinkedRegistrationDiagnosticContractTests descriptors[6].DefaultSeverity.ShouldBe(DiagnosticSeverity.Warning); descriptors[7].DefaultSeverity.ShouldBe(DiagnosticSeverity.Error); descriptors[8].DefaultSeverity.ShouldBe(DiagnosticSeverity.Error); + descriptors[9].DefaultSeverity.ShouldBe(DiagnosticSeverity.Warning); descriptors.ShouldAllBe(descriptor => descriptor.Category == AtomUIDiagnosticCategories.LinkedRegistration); } @@ -60,6 +62,8 @@ public sealed class LinkedRegistrationDiagnosticContractTests messages[6].ShouldContain("AtomUIPackageRoot"); messages[7].ShouldContain("UseXxxControls"); messages[8].ShouldContain("registration entry"); + messages[9].ShouldContain("AtomUIRegistrationUnitRoot"); + messages[9].ShouldContain("AtomUIPackageRoot"); } private static DiagnosticDescriptor[] GetDescriptors() @@ -74,7 +78,8 @@ public sealed class LinkedRegistrationDiagnosticContractTests AtomUIDiagnosticDescriptors.LinkedManifestVersionMismatch, AtomUIDiagnosticDescriptors.LinkedLooseAxamlWidened, AtomUIDiagnosticDescriptors.LinkedPackageEntryMissing, - AtomUIDiagnosticDescriptors.LinkedPackageEntryInvalid + AtomUIDiagnosticDescriptors.LinkedPackageEntryInvalid, + AtomUIDiagnosticDescriptors.LinkedDynamicUsageUncovered ]; } } diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationUsageGeneratorTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationUsageGeneratorTests.cs index e97cc902b..1cb903626 100644 --- a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationUsageGeneratorTests.cs +++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationUsageGeneratorTests.cs @@ -524,7 +524,7 @@ public sealed class LinkedRegistrationUsageGeneratorTests } [Fact] - public void Unsupported_Dynamic_Use_Widens_Only_The_Affected_Package() + public void Unsupported_Dynamic_Use_Warns_Without_Widening() { var roots = UsageGeneratorTestHost.Axaml( "obj/AtomUIAxamlUsage.xml", @@ -535,10 +535,26 @@ public sealed class LinkedRegistrationUsageGeneratorTests UsageGeneratorTestHost.LinkedEntryOptions, additionalTexts: [roots]); + // The only PackageRoot comes from the explicit AtomUIPackageRoot declaration; + // the unresolvable dynamic creation site itself no longer widens the package. result.Usages.Count(usage => usage.Kind == LinkedUsageKind.PackageRoot && usage.Identity == "Acme.Controls").ShouldBe(1); result.Diagnostics.ShouldContain(diagnostic => - diagnostic.Id == "ATOMUILINK002" && diagnostic.Severity == DiagnosticSeverity.Warning); + diagnostic.Id == "ATOMUILINK010" && diagnostic.Severity == DiagnosticSeverity.Warning); + result.Diagnostics.ShouldNotContain(diagnostic => diagnostic.Id == "ATOMUILINK002"); + } + + [Fact] + public void Unsupported_Dynamic_Use_With_Invoked_Entry_Warns_Without_A_Package_Root() + { + var result = UsageGeneratorTestHost.Run( + ["using System; public sealed class Consumer { public static void Run() { Other.Controls.Entry.UseOtherControls(); } public object? Create(Type type) => Activator.CreateInstance(type); }"], + [UsageGeneratorTestHost.AcmePackage, UsageGeneratorTestHost.OtherPackage], + UsageGeneratorTestHost.LinkedEntryOptions); + + result.Usages.ShouldNotContain(usage => usage.Kind == LinkedUsageKind.PackageRoot); + result.Diagnostics.ShouldContain(diagnostic => + diagnostic.Id == "ATOMUILINK010" && diagnostic.Severity == DiagnosticSeverity.Warning); } [Fact] @@ -662,7 +678,7 @@ public sealed class LinkedRegistrationUsageGeneratorTests warning.Usages.ShouldBe(strict.Usages); strict.Diagnostics.ShouldContain(diagnostic => - diagnostic.Id == "ATOMUILINK002" && diagnostic.Severity == DiagnosticSeverity.Error); + diagnostic.Id == "ATOMUILINK010" && diagnostic.Severity == DiagnosticSeverity.Error); } [Fact] diff --git a/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs b/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs index 365fef6c0..0f87d01ae 100644 --- a/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs +++ b/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs @@ -459,7 +459,7 @@ public class ThemeSchemaGeneratorTests } [Fact] - public void Directory_Granularity_Unresolved_Dynamic_Control_Creation_Forces_Package_Fallback() + public void Directory_Granularity_Unresolved_Dynamic_Control_Creation_Warns_Without_Widening() { var compilation = CreateCompilation( """ @@ -486,7 +486,7 @@ public class ThemeSchemaGeneratorTests "Directory"); var diagnostic = diagnostics.ShouldHaveSingleItem(); - diagnostic.Id.ShouldBe("ATOMUILINK002"); + diagnostic.Id.ShouldBe("ATOMUILINK010"); diagnostic.Location.GetLineSpan().Path.ShouldBe("Button/Button.cs"); var source = GetGeneratedSource(outputCompilation, "LinkedRegistrationPackageAnalysis.g.cs"); source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.Fallback.v1\""); @@ -494,7 +494,7 @@ public class ThemeSchemaGeneratorTests } [Fact] - public void Directory_Granularity_Dynamic_Invocation_Forces_Package_Fallback() + public void Directory_Granularity_Dynamic_Invocation_Warns_Without_Widening() { var compilation = CreateCompilation( """ @@ -513,7 +513,7 @@ public class ThemeSchemaGeneratorTests "Directory"); var diagnostic = diagnostics.ShouldHaveSingleItem(); - diagnostic.Id.ShouldBe("ATOMUILINK002"); + diagnostic.Id.ShouldBe("ATOMUILINK010"); diagnostic.Location.GetLineSpan().Path.ShouldBe("Button/Button.cs"); var source = GetGeneratedSource(outputCompilation, "LinkedRegistrationPackageAnalysis.g.cs"); source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.Fallback.v1\"");