From 9593bf153775770de8a36d65e76da4e1b567d67f Mon Sep 17 00:00:00 2001 From: chinboy Date: Mon, 17 Aug 2026 15:18:29 +0800 Subject: [PATCH] fix(AOT): bound linked registration analysis and static unit planning --- AGENTS.md | 2 + AtomUI.slnx | 1 + build/AtomUI.Generator.props | 1 + build/AtomUI.Generator.targets | 13 + build/AtomUI.GeneratorConsumer.targets | 2 + ...LinkedRegistration.SidecarConsumer.targets | 7 + build/AtomUI.LinkedRegistration.props | 2 + build/AtomUI.LinkedRegistration.targets | 96 ++- build/AtomUI.Repository.props | 11 + build/ProjectDefaults.props | 2 +- .../AtomUIGallery.Browser.csproj | 8 +- .../foundations/aot-and-trimming.md | 585 +++++----------- .../aot-linked-registration-pipeline.md | 299 +++++++++ .../aot-registration-unit-granularity.md | 19 +- .../foundations/build-and-packaging.md | 40 +- docs/architecture/foundations/overview.md | 2 + .../foundations/startup-and-registration.md | 12 +- docs/architecture/overview.md | 3 +- .../desktop/general/button/implementation.md | 6 +- .../general/split-button/implementation.md | 2 +- .../dropdown-button/implementation.md | 2 +- ...dynamic-resource-memory-leak-case-study.md | 4 +- .../development/aot-programming-guidelines.md | 36 +- .../theming/third-party-control-packages.md | 20 +- docs/modules/generator/linked-registration.md | 227 +++++++ docs/modules/generator/overview.md | 21 +- docs/overview.md | 1 + .../aot/linked-registration-sidecar.md | 171 +++++ docs/reference/aot/overview.md | 7 + docs/reference/overview.md | 1 + ...aot-sidecar-manifest-static-plan-design.md | 629 ++++++++++++++++++ .../verify-aot-trim-registration.sh | 85 ++- .../AtomUI.Build.Tasks.csproj | 6 + .../GenerateLinkedRegistrationSidecarTask.cs | 440 ++++++++++++ .../LinkedRegistrationAnalysisBudget.cs | 18 + .../LinkedRegistrationSidecar.cs | 350 ++++++++++ ...otTrimControlPackageRegistrationBuilder.cs | 4 +- .../{Buttons => Button}/Button.cs | 0 .../{Buttons => Button}/ButtonToken.cs | 0 .../{Buttons => Button}/HyperLinkButton.cs | 0 .../HyperLinkButtonToken.cs | 0 .../{Buttons => Button}/IconButton.cs | 0 .../{Buttons => Button}/ToggleIconButton.cs | 0 .../DropdownButton.cs | 0 .../DropdownButtonToken.cs | 0 .../DialogInputCaptureTracker.cs | 0 .../ToolTipService.cs | 0 .../{Buttons => SplitButton}/SplitButton.cs | 0 .../SplitButtonToken.cs | 0 .../AtomUI.Generator.LinkedPublish.csproj | 48 ++ .../LinkedCSharpUsageCandidateProvider.cs | 352 ++++++++++ ...kedRegistrationPackageManifestGenerator.cs | 497 ++++++++++++++ .../RegistrationUnitGraphPlanner.cs | 254 +++++++ src/AtomUI.Generator/AtomUI.Generator.csproj | 20 + .../ApplicationRegistrationPlanGenerator.cs | 94 ++- .../LinkedRegistrationManifestCatalog.cs | 426 +++++++++++- .../LinkedRegistrationProtocol.cs | 3 + .../LinkedRegistrationUsageGenerator.cs | 501 +++++++------- .../LinkedRegistrationManifestCodec.cs | 104 ++- .../LinkedRegistrationManifestModels.cs | 98 +++ .../ControlPackageRegistrationWriter.cs | 4 + .../ThemeAssets/ThemeAssetManifestWriter.cs | 39 +- .../ThemeSchema/GeneratedThemeSchemaWriter.cs | 41 +- .../TokenResourceKeyGenerator.cs | 47 +- ...lsGeneralLayoutThemeScopeMigrationTests.cs | 10 +- .../AtomUI.Generator.Tests.csproj | 2 + .../BuildInfrastructure/BuildLayoutTests.cs | 34 + ...plicationRegistrationPlanGeneratorTests.cs | 44 +- .../LinkedRegistrationBuildAssetsTests.cs | 103 ++- ...gistrationPackageManifestGeneratorTests.cs | 306 +++++++++ .../LinkedRegistrationPublishFixturesTests.cs | 34 +- .../LinkedRegistrationUsageGeneratorTests.cs | 251 +++++-- .../PackageEntryIntegrationTests.cs | 21 + .../RegistrationUnitGraphPlannerTests.cs | 63 ++ .../ThemeAssetManifestGeneratorTests.cs | 28 +- .../ThemeSchemaGeneratorTests.cs | 61 +- .../Minimal/Minimal.csproj | 5 + .../UnusedUnit/UnusedUnitFragment.cs | 32 +- .../GalleryDeveloperToolsConventionsTests.cs | 5 + 79 files changed, 5681 insertions(+), 981 deletions(-) create mode 100644 build/AtomUI.LinkedRegistration.SidecarConsumer.targets create mode 100644 docs/architecture/foundations/aot-linked-registration-pipeline.md create mode 100644 docs/modules/generator/linked-registration.md create mode 100644 docs/reference/aot/linked-registration-sidecar.md create mode 100644 docs/reference/aot/overview.md create mode 100644 docs/superpowers/specs/2026-08-16-aot-sidecar-manifest-static-plan-design.md create mode 100644 src/AtomUI.Build.Tasks/GenerateLinkedRegistrationSidecarTask.cs create mode 100644 src/AtomUI.Build.Tasks/LinkedRegistration/LinkedRegistrationAnalysisBudget.cs create mode 100644 src/AtomUI.Build.Tasks/LinkedRegistration/LinkedRegistrationSidecar.cs rename src/AtomUI.Desktop.Controls/{Buttons => Button}/Button.cs (100%) rename src/AtomUI.Desktop.Controls/{Buttons => Button}/ButtonToken.cs (100%) rename src/AtomUI.Desktop.Controls/{Buttons => Button}/HyperLinkButton.cs (100%) rename src/AtomUI.Desktop.Controls/{Buttons => Button}/HyperLinkButtonToken.cs (100%) rename src/AtomUI.Desktop.Controls/{Buttons => Button}/IconButton.cs (100%) rename src/AtomUI.Desktop.Controls/{Buttons => Button}/ToggleIconButton.cs (100%) rename src/AtomUI.Desktop.Controls/{Buttons => DropdownButton}/DropdownButton.cs (100%) rename src/AtomUI.Desktop.Controls/{Buttons => DropdownButton}/DropdownButtonToken.cs (100%) rename src/AtomUI.Desktop.Controls/{Dialog => PackageCore}/DialogInputCaptureTracker.cs (100%) rename src/AtomUI.Desktop.Controls/{Tooltip => PackageCore}/ToolTipService.cs (100%) rename src/AtomUI.Desktop.Controls/{Buttons => SplitButton}/SplitButton.cs (100%) rename src/AtomUI.Desktop.Controls/{Buttons => SplitButton}/SplitButtonToken.cs (100%) create mode 100644 src/AtomUI.Generator.LinkedPublish/AtomUI.Generator.LinkedPublish.csproj create mode 100644 src/AtomUI.Generator.LinkedPublish/LinkedCSharpUsageCandidateProvider.cs create mode 100644 src/AtomUI.Generator.LinkedPublish/LinkedRegistrationPackageManifestGenerator.cs create mode 100644 src/AtomUI.Generator.LinkedPublish/RegistrationUnitGraphPlanner.cs create mode 100644 tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationPackageManifestGeneratorTests.cs create mode 100644 tests/AtomUI.Generator.Tests/LinkedRegistration/RegistrationUnitGraphPlannerTests.cs diff --git a/AGENTS.md b/AGENTS.md index 75cea4e9d..f93435171 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,7 +85,9 @@ Read the relevant document before touching the corresponding area: - Localization target architecture: [docs/architecture/systems/localization/overview.md](docs/architecture/systems/localization/overview.md) - AI collaboration and bug-fix discipline: [docs/engineering/contributing/agent-guidelines.md](docs/engineering/contributing/agent-guidelines.md) - AOT and trimming architecture: [docs/architecture/foundations/aot-and-trimming.md](docs/architecture/foundations/aot-and-trimming.md) +- AOT linked registration pipeline: [docs/architecture/foundations/aot-linked-registration-pipeline.md](docs/architecture/foundations/aot-linked-registration-pipeline.md) - AOT Registration Unit granularity: [docs/architecture/foundations/aot-registration-unit-granularity.md](docs/architecture/foundations/aot-registration-unit-granularity.md) +- Linked registration generator: [docs/modules/generator/linked-registration.md](docs/modules/generator/linked-registration.md) - AOT programming, dynamic data, source generators: [docs/engineering/development/aot-programming-guidelines.md](docs/engineering/development/aot-programming-guidelines.md) - Compiler diagnostics: [docs/engineering/development/compiler-diagnostics-guidelines.md](docs/engineering/development/compiler-diagnostics-guidelines.md) - Documentation structure and file naming: [docs/engineering/contributing/documentation-structure-guidelines.md](docs/engineering/contributing/documentation-structure-guidelines.md) diff --git a/AtomUI.slnx b/AtomUI.slnx index d2a1bbab9..871568095 100644 --- a/AtomUI.slnx +++ b/AtomUI.slnx @@ -13,6 +13,7 @@ + diff --git a/build/AtomUI.Generator.props b/build/AtomUI.Generator.props index 69fa699ee..5bf521d94 100644 --- a/build/AtomUI.Generator.props +++ b/build/AtomUI.Generator.props @@ -1,6 +1,7 @@ $(MSBuildThisFileDirectory)../tools/netstandard2.0/AtomUI.Build.Tasks.dll + $(MSBuildThisFileDirectory)../tools/netstandard2.0/AtomUI.Generator.LinkedPublish.dll true + + + + <_AtomUIExistingLinkedPublishAnalyzer Include="@(Analyzer)" + Condition="'%(Filename)' == 'AtomUI.Generator.LinkedPublish'" /> + + + + diff --git a/build/AtomUI.GeneratorConsumer.targets b/build/AtomUI.GeneratorConsumer.targets index c8c53a839..f780694f1 100644 --- a/build/AtomUI.GeneratorConsumer.targets +++ b/build/AtomUI.GeneratorConsumer.targets @@ -5,6 +5,8 @@ + + diff --git a/build/AtomUI.LinkedRegistration.SidecarConsumer.targets b/build/AtomUI.LinkedRegistration.SidecarConsumer.targets new file mode 100644 index 000000000..07a80d52f --- /dev/null +++ b/build/AtomUI.LinkedRegistration.SidecarConsumer.targets @@ -0,0 +1,7 @@ + + + + + diff --git a/build/AtomUI.LinkedRegistration.props b/build/AtomUI.LinkedRegistration.props index 4aeaa4107..6a3b55e8b 100644 --- a/build/AtomUI.LinkedRegistration.props +++ b/build/AtomUI.LinkedRegistration.props @@ -1,6 +1,7 @@ false + false Package @@ -11,5 +12,6 @@ + diff --git a/build/AtomUI.LinkedRegistration.targets b/build/AtomUI.LinkedRegistration.targets index ba00f8693..18d634551 100644 --- a/build/AtomUI.LinkedRegistration.targets +++ b/build/AtomUI.LinkedRegistration.targets @@ -3,28 +3,57 @@ true true true - true + true false true false <_AtomUICollectAxamlUsage - Condition="'$(MSBuildProjectName)' != 'AtomUI.Build.Tasks'">true + Condition="'$(MSBuildProjectName)' != 'AtomUI.Build.Tasks' and ('$(AtomUILinkedPublish)' == 'true' or '$(AtomUIEmitLinkedManifest)' == 'true')">true + <_AtomUICollectAxamlUsage Condition="'$(_AtomUICollectAxamlUsage)' == ''">false $(IntermediateOutputPath)AtomUIAxamlUsage.xml + $(TargetPath).atomui-link.json + + + + + + <_AtomUILinkedReferenceSidecarCandidate + Include="@(ReferencePath->'%(FullPath).atomui-link.json')" /> + <_AtomUIProjectReferenceSidecar + Include="@(_AtomUILinkedReferenceSidecarCandidate)" + Condition="Exists('%(Identity)')" /> + + + + + + + + + BeforeTargets="GenerateMSBuildEditorConfigFileShouldRun;CoreCompile" + Condition="'$(_AtomUICollectAxamlUsage)' == 'true'"> <_AtomUIHasAxamlUsageInput Condition="'@(AvaloniaXaml)' != '' or '@(AtomUIRegistrationUnitRoot)' != '' or '@(AtomUIPackageRoot)' != ''">true @@ -50,6 +79,67 @@ + + + + + + + + + + + + + <_AtomUILinkedRegistrationSidecarTargetOutput + Include="$(AtomUILinkedSidecarOutputPath)" + Condition="Exists('$(AtomUILinkedSidecarOutputPath)')" /> + <_AtomUILinkedRegistrationSidecarTargetOutput + Remove="@(_AtomUILinkedRegistrationSidecarTargetOutput)" + Condition="!Exists('%(Identity)')" /> + + + + + + + <_AtomUIPackSidecarTargetFramework Condition="'$(TargetFramework)' != ''">$(TargetFramework) + <_AtomUIPackSidecarTargetFramework Condition="'$(_AtomUIPackSidecarTargetFramework)' == '' and '$(TargetFrameworks)' != ''">$([System.String]::Copy('$(TargetFrameworks)').Split(';').GetValue(0)) + + + + + + + + + + + true $(OutputPathWithoutFramework)/netstandard2.0/AtomUI.Build.Tasks.dll + $(OutputPathWithoutFramework)/netstandard2.0/AtomUI.Generator.LinkedPublish.dll @@ -13,6 +15,7 @@ $(MSBuildThisFileDirectory)AtomUI.Generator.targets; $(MSBuildThisFileDirectory)AtomUI.LinkedRegistration.props; $(MSBuildThisFileDirectory)AtomUI.LinkedRegistration.targets; + $(MSBuildThisFileDirectory)AtomUI.LinkedRegistration.SidecarConsumer.targets; $(MSBuildThisFileDirectory)AtomUI.Localization.props; $(MSBuildThisFileDirectory)AtomUI.Localization.targets; $(MSBuildThisFileDirectory)AtomUI.ThemeAssets.targets"> @@ -20,6 +23,7 @@ + + + diff --git a/build/ProjectDefaults.props b/build/ProjectDefaults.props index 622e2f655..5c6a4b406 100644 --- a/build/ProjectDefaults.props +++ b/build/ProjectDefaults.props @@ -15,7 +15,7 @@ $(AtomUIDevelopTargetFramework);$(AtomUIProductionTargetFramework) true - $(DefaultItemExcludes);$(MSBuildProjectDirectory)/obj/** + $(DefaultItemExcludes);$(MSBuildProjectDirectory)/obj/**;$(MSBuildProjectDirectory)/GeneratedFiles/** true $(NoWarn);CS1591;CS0436;CS7035;AVA3001;AVALONIA_X11_CSD $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb diff --git a/controlgallery/AtomUIGallery.Browser/AtomUIGallery.Browser.csproj b/controlgallery/AtomUIGallery.Browser/AtomUIGallery.Browser.csproj index 27ff5d902..4f9921d56 100644 --- a/controlgallery/AtomUIGallery.Browser/AtomUIGallery.Browser.csproj +++ b/controlgallery/AtomUIGallery.Browser/AtomUIGallery.Browser.csproj @@ -8,7 +8,12 @@ false false - false + + false true @@ -20,6 +25,7 @@ false diff --git a/docs/architecture/foundations/aot-and-trimming.md b/docs/architecture/foundations/aot-and-trimming.md index 882a8ef5f..076cf1278 100644 --- a/docs/architecture/foundations/aot-and-trimming.md +++ b/docs/architecture/foundations/aot-and-trimming.md @@ -1,358 +1,141 @@ # AOT 与裁剪架构 -> 状态:截至 2026-08-15,Registration Unit、应用计划、源码注册入口声明、`Package` 默认粒度与显式 `Directory` 粒度均已实现, -> 并通过 Generator、trimmed JIT、macOS NativeAOT、WebAssembly AOT 和 Gallery NativeAOT 发布验证。 +> 状态:截至 2026-08-16,本文定义 AtomUI Registration Unit、Sidecar Manifest、应用静态计划和安全 fallback 的正式架构。 -本文是 AtomUI AOT 与 trimming 整体架构的正式所有者。它覆盖 `AtomUI.Core`、`AtomUI.Controls`、 -`AtomUI.Desktop.Controls`、可选桌面包、`AtomUI.Generator`、`AtomUI.Build.Tasks`、应用项目和第三方 Control 包。 -Registration Unit 粒度和资源归属的正式契约见 -[AOT Registration Unit 粒度](aot-registration-unit-granularity.md)。日常编码规则见 -[AOT 编程规范](../../engineering/development/aot-programming-guidelines.md),具体平台发布命令由对应平台文档维护。 +本文是 AtomUI AOT 与 trimming 整体架构的正式所有者,覆盖 `AtomUI.Core`、Control Packages、Generator、Build Tasks、 +应用项目和第三方包。专项契约分别由以下文档维护: + +- [AOT Linked Registration Pipeline](aot-linked-registration-pipeline.md):Analyzer 激活、Sidecar、UnitEdge、静态计划和运行时边界。 +- [AOT Registration Unit 粒度](aot-registration-unit-granularity.md):Package/Directory 粒度和资源归属。 +- [Linked Registration Sidecar](../../reference/aot/linked-registration-sidecar.md):机器可读协议。 +- [AOT 编程规范](../../engineering/development/aot-programming-guidelines.md):日常开发和 review 规则。 +- [第三方 Control Package 指南](../../guides/theming/third-party-control-packages.md):包作者接入步骤。 ## 1. 问题定义 -AtomUI 的 Theme 和 Localization Registry 必须在首帧前完整构建并冻结。当前包级生成代码会构造完整 Control descriptor -数组和完整 Theme Asset descriptor 数组,资源加载器还通过覆盖全包 AXAML factory 的大型分发表引用所有主题资源。 -即使运行时随后按 identity 过滤,linker 在静态分析阶段已经把全部 Control、Own Token 和 AXAML factory 判定为可达。 +AtomUI 的 Theme 和 Localization Registry 必须在首帧前完整构建并冻结。构造全包 descriptor、Theme Asset 或 AXAML factory +后再运行时过滤,不能让 ILLink 删除未使用控件;反射扫描、linker XML、控件实例化后追加注册又不能同时满足 NativeAOT、 +冻结时序和作者体验。 -问题根因是注册入口的静态可达性粒度。通过运行时筛选、反射扫描、`DynamicDependency`、linker XML 或控件实例化后的 -late registration,都不能同时满足可裁剪、NativeAOT 和 Registry 冻结契约。 +AtomUI 因此把静态可达性拆成 Registration Unit。普通包以整个 Package 为安全 Unit;只有大型多控件包才按稳定控件族拆分。 +应用 AOT/Trim 编译读取纯构建期 Sidecar,计算 Unit closure,并生成最终强类型静态调用。 -Avalonia Fluent Theme 采用包级静态主题注册来保证正确性。AtomUI 在保持同等正确性的基础上,让 Package 明确选择 -Registration Unit 粒度:普通包以整个 Package 作为安全单元,大型多控件包才按稳定控件族拆分,不追求每个内部 CLR -类型独立裁剪。 +## 2. 架构不变量 -## 2. 设计目标 +1. 应用继续只调用真实入口,例如 `builder.UseDesktopControls()`。 +2. 普通 Debug 和未启用 AOT/Trim 的 Release 使用完整兼容注册,不加载 linked-publish Analyzer。 +3. `PublishTrimmed=true`、`PublishAot=true` 和 `RunAOTCompilation=true` 自动使用应用静态计划。 +4. 普通第三方包默认只有一个 Package Unit,不声明 Unit dependency、ownership 修补或 linker XML。 +5. 运行时不读取 Sidecar、不遍历 Unit 图、不扫描程序集、类型、AXAML、Attribute 或资源目录。 +6. Registry 仍在首帧前一次性构建并冻结,不引入 late registration。 +7. Unit fragment 是叶子,不调用其他 Unit,也不运行时去重。 +8. 无法证明精确 Unit 集合时,只扩大为对应 Package full registrar。 +9. Language、Provider、Global Token、Theme Algorithm 和初始化逻辑保持 Package 级语义与顺序。 +10. 分析必须增量且有确定性结构预算;超限触发 fallback,不能形成无界编译或巨大 linker 方法图。 -本架构必须满足: +对于任意动态程序集、反射、脚本或发布后插件,不可能同时保证理论最小体积、作者零声明和运行时零发现。本架构保证后两项和 +运行正确性;静态证据不足时牺牲体积。 -1. 普通非裁剪构建继续使用现有全量注册语义,不要求应用迁移。 -2. `PublishTrimmed=true`、`PublishAot=true` 和 `RunAOTCompilation=true` 自动使用生成式 Unit 注册。 -3. 应用继续调用 `UseDesktopControls()`,普通静态 AXAML/C# 使用不增加配置步骤。 -4. 运行时不扫描程序集、类型、AXAML、Attribute 或资源目录。 -5. Registry 仍在首帧前一次性构建并冻结,不引入 late registration。 -6. 完全未使用的控件族可以连同 descriptor、Own Token、内部控件和 AXAML factory 一起删除。 -7. 无法证明精确 Unit 集合时,只能扩大到对应 Package 的全量注册,不能静默生成不完整主题。 -8. 语言、全局主题基础设施和包级初始化行为保持当前语义与顺序。 -9. 第三方 Control 包可以使用同一版本化 Unit 协议,不依赖 AtomUI 内部程序集扫描。 -10. 发布验证同时覆盖运行行为、fallback 和可重复的体积差值。 -11. Package 注册入口以真实 C# 方法作为唯一事实来源,不要求包作者在 MSBuild 中重复维护 CLR metadata name。 -12. 普通第三方包默认只有一个 Package Unit,不要求作者声明 Unit ownership 或内部依赖图。 +## 3. 构建模式 -## 3. 复杂度边界 +| 构建模式 | 注册模式 | Linked Analyzer | +| --- | --- | --- | +| 普通 `dotnet build` / `dotnet run` | full registrar | 不加载 | +| 未裁剪 Release、self-contained、ReadyToRun | full registrar | 不加载 | +| `PublishTrimmed=true` | application static plan | 加载 | +| `PublishAot=true` | application static plan | 加载 | +| `RunAOTCompilation=true` | application static plan | 加载 | +| 显式 `UseAllDesktopControls()` | full registrar | 不改变构建模式 | -本架构明确不引入: +Build Targets 把真实 linker 条件规范化为内部 `AtomUILinkedPublish`,并配置 linker 可替换的 +`AtomUI.AotTrimRegistration.Enabled` feature switch。`AtomUIUseGeneratedRegistration=true` 不得隐式加载完整应用分析;仓库测试 +通过隔离 target 验证非裁剪 generated path。 -- Feature Manifest、Feature Root 或 Feature Phase。 -- Catalog Fragment 或按语言闭包。 -- 每个 Theme Asset 的跨程序集 Manifest。 -- Semantic Part、Initializer、Catalog 等应用级图节点。 -- 应用级 BFS、循环闭包或通用依赖图规划器。 -- 运行时反射发现、AXAML 扫描或动态代码生成。 -- 将无法解析 owner 的资源自动归类为共享资源。 -- 根据方法名约定或方法体调用图猜测 Package 注册入口。 -- 使用 MSBuild 字符串重复声明注册方法的 CLR metadata name。 +普通构建必须同时满足:compiler command line 不含 linked Analyzer、AXAML/Sidecar targets skipped、`obj` 中没有 linked usage、 +Sidecar 或 Application Plan。 -Registration Unit 是唯一细粒度裁剪单位,但不是所有包都必须细分。语言、初始化器和共享基础资源按 Package 保留,以 -少量固定体积换取稳定行为和低维护成本。 +## 4. Registration Unit -### 3.1 AOT/Trim 基础设施命名边界 - -AOT/Trim 注册路径属于发布基础设施,不是通用运行时功能。`AtomUI.Core` 中跨程序集调用的隐藏运行时 ABI 必须使用 -明确的 `AotTrim` 前缀,并且不在类型名中重复 `AtomUI`;类型已经位于 `AtomUI.Registration` 命名空间内。当前命名契约为: - -| 类型 | 职责 | -| --- | --- | -| `AotTrimRegistration` | 读取发布期注册模式标记,暴露 `IsEnabled`;不执行注册、不执行裁剪 | -| `AotTrimControlPackageRegistrationBuilder` | 收集已选 Registration Unit 的 descriptor 和 Theme asset,并提交一个完整 Package registration | -| `AotTrimRegistrationPlan` | 应用级 AOT/Trim Package plan 的跨程序集调用协议 | -| `AotTrimRegistrationPlanRegistry` | 由模块初始化器安装唯一 plan,并按 Package 分派调用 | - -对应的 AppContext key 必须保留 `AtomUI` 前缀,以避免全局字符串冲突: - -```text -AtomUI.AotTrimRegistration.Enabled -``` - -普通运行时功能不得依赖这些类型或开关。`GeneratedApplicationRegistrationPlan`、`Generated*UnitFragment` 等名称只用于 -Source Generator 产生的内部代码;它们描述生成物,不构成用户运行时 API。Generator 内部的 `LinkedRegistration*` -类型位于明确的 `AtomUI.Generator.LinkedRegistration` 命名空间,可以保留协议实现语义;Build Task 则使用动作加对象的 -名称,例如 `CollectAxamlUsageTask` 和 `ValidateAssemblyMetadataMarkerTask`。 - -包级生成代码使用 `AtomUI.Generated.`,其中 `AssemblyOwner` 是程序集名折叠得到的单一 PascalCase -标识符,分隔符不保留。例如 `AtomUI.Desktop.Controls` 对应 `AtomUI.Generated.AtomUIDesktopControls`。Theme Schema、 -Theme Asset、Localization、Linked Registration 和 AXAML wrapper 必须共享这一命名规则;该名称会进入编译引用和 -Assembly Metadata,属于需要稳定验证的内部生成 ABI。 - -## 4. 构建模式契约 - -AtomUI 以是否进入 linker 处理作为注册模式边界: - -| 构建模式 | `UseDesktopControls()` 注册模式 | -| --- | --- | -| 普通 `dotnet build` / `dotnet run` | 全量兼容注册 | -| 非裁剪 Release、SelfContained、ReadyToRun | 全量兼容注册 | -| `PublishTrimmed=true`,任意 `TrimMode` | 生成式 Unit 注册 | -| `PublishAot=true` | 生成式 Unit 注册 | -| `RunAOTCompilation=true`,WebAssembly | 生成式 Unit 注册 | -| `AtomUIUseGeneratedRegistration=true` | 生成式 Unit 注册,用于非裁剪环境验证 | -| 显式 `UseAllDesktopControls()` | 全量注册 | - -Build Targets 把上述条件规范化为内部 `AtomUILinkedPublish`,并把 -`AtomUI.AotTrimRegistration.Enabled` 设置为 linker 可替换的 AOT/Trim feature switch。Analyzer 和库声明不单独改变注册模式。 - -普通非裁剪路径必须保持 Common、Desktop 和可选包的 descriptor、asset、Catalog、Bundle、Provider 和 initializer -集合及顺序。允许的非行为差异仅限一次缓存的 feature-switch 读取和少量编译器 metadata。 - -## 5. Registration Unit - -Registration Unit 表示一个可以独立保留或删除的稳定控件族。例如: +Registration Unit 是唯一细粒度裁剪单位,不等于单个 CLR 类型。一个 Unit 包含可以独立运行的控件族: ```text DatePicker Unit ├── DatePicker / RangeDatePicker ├── Presenter、Cell 和内部辅助控件 -├── Control descriptor +├── Control descriptors ├── Own Token schema -├── 控件族专属 AXAML factory -└── 同 Package 内直接依赖的 Unit +└── 控件族专属 AXAML factories ``` -使用 Unit 中任一公开 Control 时,保留整个 Unit。内部 Presenter、Cell、Semantic Part 或命名基础 Theme 不继续拆分。 -这保证 Unit 自身是完整可运行的主题注册单元,不要求应用 Generator 理解控件内部实现。 +默认 `Package` 粒度把当前 Control Package 的所有 Control-owned 内容放入一个 Unit。显式 `Directory` 粒度才按稳定控件族拆分。 +Unit ownership、`AtomUIRegistrationUnit` 和 `AtomUIPackageSharedTheme` 的使用边界由 +[AOT Registration Unit 粒度](aot-registration-unit-granularity.md)定义。 -Unit 粒度由 Control Package 显式或默认选择: - -```xml -Package -``` - -| 粒度 | 行为 | 适用范围 | -| --- | --- | --- | -| `Package` | 当前 Package 的所有 Control-owned 内容生成一个完整 Unit | 默认值;第三方包和单一控件族包 | -| `Directory` | 按稳定控件族目录生成多个 Unit | 显式高级优化;大型多控件包 | - -未声明属性时必须等价于 `Package`。Theme Schema、Theme Asset、ControlMap 和 linked registration Generator 必须读取 -同一个规范化粒度策略;不允许一个 Generator 按 Package、另一个 Generator 按目录推导。 - -`Package` 模式的 Unit identity 由 Generator 从 Package identity 稳定派生,对普通作者不可见。应用使用包内任一 Control -时保留整个 Package Unit。`Directory` 模式才暴露可独立裁剪的控件族 Unit。 - -粒度选择、第一方 Package 映射、`AtomUIRegistrationUnit` 使用边界和第三方包完整契约由 -[AOT Registration Unit 粒度](aot-registration-unit-granularity.md)统一定义;本节只说明 Unit 如何参与整体 linked plan。 - -每个 Unit 生成一个跨程序集可调用的强类型入口: +每个 Unit 生成一个跨程序集可调用的 leaf entry: ```csharp -public static void Add( - AotTrimControlPackageRegistrationBuilder builder) +public static void Add(AotTrimControlPackageRegistrationBuilder builder) ``` -入口位于版本化生成命名空间并标记 `EditorBrowsableState.Never`。Unit 方法先调用 -`builder.TryEnterUnit(unitId)`;同 Package 依赖通过 Unit 方法之间的直接调用表达,重复依赖和循环由 builder 去重。 -应用计划不展开或重新计算这些依赖。 +入口只添加本 Unit descriptor、Theme Asset 和 resource factory。UnitEdge 写入 Sidecar;应用编译期计算 closure/SCC,并让每个 +fragment 最多出现一次。禁止生成 `AddDependencies`、调用其他 Unit 或调用 `TryEnterUnit`。 -Unit fragment 禁止引用全包 descriptor 数组、全包 Theme Asset 数组或包含全部 AXAML factory 的分发表。 +## 5. Package Core -## 6. Unit 归属契约 +以下内容按 Package 整体保留,不参与 Unit 拆分: -`Package` 模式不根据目录推导 ownership。当前程序集定义的 public Control、内部部件、descriptor、Own Token schema 和 -Control-owned Theme Asset 全部属于同一个 Package Unit。普通包不得为了内部目录关系填写 -`AtomUIRegistrationUnit`;C# 或 AXAML 中的包内依赖天然包含在同一 Unit 中。 - -`Directory` 模式使用源码和主题目录约定推导控件族 Unit。一个控件族目录中的公开 Control、内部部件和 `Themes/` -资源默认属于同一 Unit;Presenter、Cell、View、Semantic Part 和基础主题不能因为内部子目录继续拆分。目录结构无法表达 -的 resource-only Theme 例外可以使用编译期 MSBuild metadata 显式覆盖;metadata 只包含稳定字符串,不使用 -`typeof(Control)`。 - -ControlMap 表示 CLR Control 的定义程序集 ownership,不表示该 Control 一定拥有 Theme descriptor。定义程序集中的每个 -public、非泛型 Control 都必须归入一个 Unit 并输出 ControlMap;只有原本参与完整 Theme Schema 的 Control 才生成 -descriptor 并调用 `builder.AddControl(...)`。因此基础布局控件等 descriptorless Control 仍能把静态使用映射到正确 Unit, -但普通非裁剪 descriptor 集合不会被扩大。引用程序集中的 Control 可以继续贡献既有 descriptor 信息,但不能抢占 -ControlMap ownership。没有使用 `ControlPackageRegistrationEntryAttribute` 声明本程序集入口的基础设施程序集不输出 linked -Package、Unit 或 ControlMap metadata。 - -主题资源只能属于以下三类之一: - -| 分类 | 行为 | -| --- | --- | -| `ControlOwned` | 跟随对应 Registration Unit 保留或删除 | -| `PackageShared` | 包作者显式声明,Package 被注册时始终保留 | -| `Unknown` | 禁止猜测;使用该 Package 的生成式构建回退到全量注册 | - -`PackageShared` 只允许包含必须在没有选择任何 Control Unit 时仍随 Package 入口加载,或在 `Directory` 模式下确实跨多个 -Unit 使用且没有合理单一 owner 的资源,例如全局 Brush/Typography、Popup/Overlay 基础设施、Window decorations 或通用 -ScrollBar 基础主题。控件内部 Presenter、Item、Cell 和只服务一个控件族的命名 Theme 必须归入 `ControlOwned`。 - -显式共享资源使用构建项: - -```xml - - - -``` - -Generator 不能把 owner 解析失败当成共享声明。重复 Unit、跨目录冲突、共享资源引用私有 Unit 资源等不一致必须产生包作者诊断。 -`Package` 模式的普通 Control Theme 不需要共享声明;共享 item 不能作为修补目录 ownership 的手段。 - -只有 `Directory` 模式中无法由 public owner 自动解析、但只服务单个 Unit 的内部 resource-only Theme 才允许使用 -`AtomUIRegistrationUnit` metadata 显式归属。抽象或基础 typed theme 如果没有独立 generated resource wrapper,不得生成 -不存在的 wrapper 调用;它依靠同 Unit 中可加载的具体 ResourceDictionary 或 typed theme 被静态保留。 - -`Directory` 模式下,Generator 必须从可以证明的 C# 与 AXAML 使用生成同 Package Unit 依赖。已知循环由 Unit fragment 的 -`TryEnterUnit` 去重;无法证明 owner 或依赖时,只能把当前 Package 扩大为 full fallback,不能要求应用 Generator 猜测内部图。 - -## 7. Package Core - -以下内容按 Package 整体注册,不参与 Unit 裁剪: - -- Language Catalog 和全部内置 Translation Bundle。 -- Dialog input capture、Tooltip service、Motion animator、Responsive bootstrapper 等初始化逻辑。 +- Language Catalog 和内置 Translation Bundles。 +- Dialog、Tooltip、Motion、Responsive 等初始化逻辑。 - Global Token、Theme Algorithm 和 Package Provider。 - 平台 Asset Selector。 - 显式 `PackageShared` 主题资源。 -Package Core 保持当前入口中的执行位置。以 Desktop 为例,生成式路径顺序必须是: +以 Desktop 为例,生成式入口顺序必须保持: -1. 按现有契约注册依赖 Package;跨 Package 依赖不做 Unit 级闭包。 -2. 执行当前 package 提交前的初始化行为。 -3. 创建当前平台 Provider 并应用 Unit Plan 或 full fallback。 +1. 注册依赖 Package;跨 Package 不计算 Unit closure。 +2. 执行当前 Package 提交前初始化。 +3. 创建平台 Provider,并应用静态 Unit plan 或 full fallback。 4. 注册完整 Language Module。 -5. 按当前顺序添加 Theme initializer。 +5. 按既有顺序添加 Theme initializer。 -Desktop 对 Common 的依赖使用完整 Common 注册。`AtomUI.Controls` 的 Common 层不是可独立启用的 linked Package, -不声明 `ControlPackageRegistrationEntryAttribute`,也不输出 Package、Unit、ControlMap 或 full fragment metadata; -其 descriptor、Theme asset、Language Module 和 Provider 由 `UseDesktopControls()` 内部的完整 Common 注册保持可达。 +`AtomUI.Controls` Common 由 Desktop 完整注册,不是独立 linked Package,不声明 registration entry,也不进入应用 Package plan。 -当前架构不包含跨 Package Unit 闭包;任何改变都必须基于可复现的体积数据重新评估协议复杂度和维护成本,并作为独立 -架构变更评审。 +## 6. 构建期 Pipeline -## 8. 最小 Manifest 协议 +Control Package 在 NuGet Pack 或 linked ProjectReference 构建时自动生成 Sidecar。Sidecar 记录 Package、Unit、ControlMap、 +UnitEdge、Usage 和 Fallback;它通过 `buildTransitive` 或已解析 `ReferencePath` 旁的 companion Sidecar 传递,只作为 linked +build 的 AdditionalFile,不进入运行时程序集或 publish 目录。ProjectReference 的 Sidecar 必须在目标程序集复制到 +`TargetPath` 后生成,使首次冷构建和增量构建具有相同输入。 -跨程序集协议只保留四类记录: +Application Plan Generator: -| 记录 | 职责 | -| --- | --- | -| Package | Package ID、Generator 从标记方法派生的 entry method、full registrar、PackageShared fragment 和协议版本 | -| Unit | Unit ID 与 fragment type/method | -| ControlMap | CLR metadata name 到 Package/Unit 的映射 | -| Usage | 当前程序集静态使用的 Package/Unit/entry 和来源位置 | +1. 验证 Sidecar protocol、hash、ownership 和 fragment symbol。 +2. 汇总当前应用及传递类库的 entry、Control usage、Unit root 和 Package root。 +3. 检查被使用 Package 的真实 registration entry 已显式调用。 +4. 对 Exact Package 计算 Unit closure 和 SCC。 +5. 对不确定 Package 选择 full registrar。 +6. 按 dependency SCC 和 Package order key 生成确定性强类型调用。 -Manifest 使用版本化 `AssemblyMetadata` 编码稳定字符串,不把 `typeof(Control)` 写入 metadata。序列化、转义和解析只能在 -唯一 codec 中实现;未知 major version 必须停止生成式 publish,不能猜测兼容。 +Package 分析只使用候选驱动的直接 C#/AXAML 证据。禁止全树 `DescendantNodes()`,禁止从 invocation 递归进入 callee body, +禁止把 dependency 编码成 fragment 之间的调用。完整算法和性能预算见 +[AOT Linked Registration Pipeline](aot-linked-registration-pipeline.md)。 -普通类库也生成 Usage metadata,因为最终入口应用需要聚合引用类库中的静态控件使用。普通构建不安装运行计划,也不改变 -注册行为。无法精确归属的 Usage 和 Theme Asset 在普通构建中仍写入 PackageRoot metadata,但 `ATOMUILINK002` 和 -`ATOMUILINK007` 只在 linked publish 或 `AtomUIRegistrationStrict=true` 时报告,避免日常非裁剪构建被发布期 fallback -诊断污染。 +Package Core 调用 Control 类型上的普通静态成员只表示运行时代码依赖,不自动 root 该 Control Unit。只有直接构造、`typeof` +等 Type 证据,或方法声明直接返回具体 Control 时才生成 Package root;泛型返回值的调用点替换不作为工厂证据。Unit 内跨 +Unit 调用仍生成直接 UnitEdge,应用和普通类库的 Control 成员调用仍形成该 Control 的 usage。 -只有定义 Control 的 Package 可以输出它的 ControlMap。ControlMap 指向的 Unit 必须存在且属于同一 Package;重复 ownership、 -缺失 Unit 或跨 Package 关系均按无效 Package 定义处理。没有任何源码 registration entry 的程序集不生成空的 linked -Package metadata,避免把纯基础设施程序集误识别为可注册控件包。 +## 7. 静态使用和动态 Root -### 8.1 Package 注册入口声明 +自动发现至少覆盖: -Package 的公开注册方法是入口身份的唯一事实来源。参与 linked registration 的方法使用 -`AtomUI.Registration.ControlPackageRegistrationEntryAttribute` 标记: +- C# 显式/隐式对象构造、`typeof(...)`、Control 基类和 registration entry 调用。 +- Directory 模式同程序集跨 Unit 的直接方法或构造调用。 +- AXAML 元素类型、TargetType、BasedOn、selector、DataTemplate、ControlTemplate 和 `x:Type`。 -```csharp -[ControlPackageRegistrationEntry] -public static IAtomUIBuilder UseAcmeControls(this IAtomUIBuilder builder) -{ - // 保持普通 full 注册和生成式 Package plan 分支。 -} -``` +Control 的普通 field/property/parameter/return type 引用不等于实例化,不单独形成 Unit usage。无法验证的 generated output、 +dynamic、reflection、Loose AXAML 和插件输入触发 Package fallback。 -Attribute 不接收 Package ID、类型名或方法名。Generator 从 `IMethodSymbol` 派生稳定 CLR metadata name,规范化排序后写入 -Package manifest。方法、包含类型或命名空间重命名时,manifest 必须随真实符号自动更新,不存在第二份字符串配置。 - -Generator 必须在 Package 自身编译阶段验证标记方法: - -- 位于当前程序集,并且是静态、非泛型扩展方法。 -- 第一个参数是 `this IAtomUIBuilder`,返回值可赋给 `IAtomUIBuilder`。 -- 包含类型和方法必须是 public。 -- 标记方法不得形成无法用现有 manifest method identity 唯一表达的重载集合。 -- 同一 Package 可以标记多个明确别名,例如生成式入口和显式 full 入口;Generator 稳定排序并去重。 - -旧的入口方法 metadata 字符串属性已经删除,不再是允许的项目属性、NuGet build metadata 或第三方扩展点。不得用新的 -MSBuild item、命名约定、源码文本匹配或方法体调用图扫描重新制造第二份入口身份。 - -Attribute 只参与编译期 manifest 生成。应用运行时不读取 Attribute、不扫描程序集,也不根据 Attribute 执行注册;真正注册 -仍由用户显式调用入口方法触发。没有标记入口的程序集仍可生成普通 full registration helper,但不能输出 linked Package、 -Unit、ControlMap 或 full fragment metadata。 - -## 9. 静态使用发现 - -### 9.1 AXAML - -MSBuild task 在 `CoreCompile` 前使用 XML parser 结构化读取 `@(AvaloniaXaml)`,输出带文件、行和列的类型候选。不得使用 -正则扫描文档。至少覆盖元素类型、自定义 Control 基类、`ControlTheme.TargetType`、`BasedOn`、Style selector、 -`DataTemplate`、`ControlTemplate` 和 `x:Type`。 - -Generator 结合 Roslyn Compilation 和 ControlMap 把类型映射为 Unit。无法解析的 AtomUI 类型、Loose AXAML 或动态资源 -来源必须把对应 Package 标记为 full fallback。 - -Package 自身的 Theme Asset 还必须结构化读取模板中的元素类型。能通过 `using:`、`clr-namespace:` 或当前程序集 -`XmlnsDefinition` 精确解析到同 Package public Control 的元素,生成 owner Unit 到目标 Unit 的直接依赖;默认 Avalonia -元素、外部 Package 元素和无法证明 ownership 的同名类型不得按短名称猜测。该依赖只影响 Unit 静态可达性,不能加入 -Theme Asset descriptor 的 referenced identities,也不能改变普通非裁剪 descriptor、asset 或 fingerprint。 - -### 9.2 C# - -Directory 模式的 Package 依赖分析只把可以证明会影响运行时注册的 C# 证据加入 Unit 边:直接/隐式构造、直接 `typeof(Control)`、 -已知动态创建 API、继承的 AtomUI Control 基类,以及同一程序集内可达 helper 方法、属性 getter/setter 和字段初始化器中的实际 -控件创建。对 `new T()` 这类闭合泛型构造,Generator 会把调用点的具体类型实参替换回 helper body。 - -Control 的字段、属性、事件、参数和返回值签名本身,以及 `List`、`Enumerable.Empty()` 等普通泛型实参, -不单独形成 Unit 依赖;它们只是 API 类型引用,不等于运行时创建了该 Control。这样可以避免无关 Unit 被静态注册,同时保留真实 -实现依赖。无法观察的 Source Generator 输出、反射或动态插件输入,必须由该工具输出 Usage metadata 或由应用添加显式 root。 - -其他 Source Generator 产生而当前 Generator 无法观察的 AtomUI 使用,必须由该工具输出 Usage metadata 或由应用添加显式 root。 - -保守误保留是允许的;静默漏保留不允许。 - -## 10. 应用计划 - -入口程序集聚合 Package、Unit、ControlMap 和所有引用程序集的 Usage 记录。它只完成: - -1. 将 Control 使用映射为 Unit。 -2. 按 Package 稳定排序和去重 Unit。 -3. 为存在不确定性的 Package 选择 full fallback。 -4. 生成按 Package ID 分派的强类型直接调用。 - -计划不遍历 Theme Asset、Catalog、Feature 或 Initializer 图。生成代码结构固定为: - -```csharp -case "AtomUI.Desktop.Controls": - GeneratedDesktopPackageSharedFragment.Add(packageBuilder); - GeneratedButtonUnitFragment.Add(packageBuilder); - GeneratedDatePickerUnitFragment.Add(packageBuilder); - packageBuilder.Register(); - return true; -``` - -full fallback case 直接调用该 Package 的版本化 full registrar。fallback 在编译期确定;运行时不尝试发现缺失 Unit,也不从 -异常重新引用 full registrar。 - -模块初始化器只把 plan delegate 安装到 `AotTrimRegistrationPlanRegistry`。真正的 Theme、Language 和 initializer -注册仍由用户调用的 `UseXxxControls()` 入口触发,保持可选包 gating 和调用顺序。 - -## 11. 动态输入与安全回退 - -安全策略是单调扩大:任何不确定性只能从 Unit 集合扩大为 Package full,不能缩小注册范围。 - -| 场景 | 结果 | -| --- | --- | -| 静态 Control 可确定 | 注册对应 Unit | -| 自定义 Control 的 AtomUI 基类可确定 | 注册基类 Unit | -| Unit 内部依赖 | 由 Unit fragment 直接调用并去重 | -| 显式 `PackageShared` | Package 注册时始终保留 | -| owner、AXAML 类型或已知动态 API 无法解析 | 对应 Package full fallback,并警告 | -| 旧第三方包只有 full registrar | 仅该第三方 Package full fallback,并警告 | -| Manifest major version 不兼容 | 构建错误 | -| 检测到 Control 使用但缺少对应 `UseXxxControls()` | 构建错误 | - -显式 roots 只保留两类: +消费应用处理真正动态输入时可以显式声明: ```xml @@ -361,154 +144,128 @@ full fallback case 直接调用该 Package 的版本化 full registrar。fallbac ``` -`AtomUIRegistrationUnitRoot` 保留指定 Unit;`AtomUIPackageRoot` 强制对应 Package 使用 full registrar。完全不可观察的字符串、 -网络数据或发布后插件必须使用 Package root。`AtomUIRegistrationStrict=true` 把自动 full fallback warning 提升为 error, -但不改变已计算的保留范围。 +`AtomUIRegistrationUnitRoot` 保留指定 Unit;`AtomUIPackageRoot` 强制对应 Package full registrar。它们不是普通 Control Package 的 +接入步骤。 -## 12. 公开入口与兼容性 +## 8. 安全 Fallback -用户入口保持: +分析结果只有 Exact 和 PackageFallback。安全策略是单调扩大: + +| 场景 | 结果 | +| --- | --- | +| 静态 Control/AXAML 可确定 | 选择对应 Unit closure | +| 默认 Package 粒度 | 选择完整 Package Unit | +| Unit cycle | SCC 全选,每个 leaf fragment 调用一次 | +| dynamic/reflection/Loose AXAML | 对应 Package full registrar | +| Sidecar 缺失、陈旧或无法验证 | 对应 Package full registrar | +| 分析预算超限 | 对应 Package full registrar | +| 未知 protocol major | 构建 Error | +| Fragment symbol 不存在 | 构建 Error | +| 使用 Package 但未调用 entry | 构建 Error | + +Fallback 在编译期确定。运行时不捕获缺失资源后重试,不重新引用 full registrar,也不扫描 Package。 + +## 9. 公开入口与运行时 ABI + +Package 的公开注册方法是入口身份的唯一事实来源: ```csharp [ControlPackageRegistrationEntry] -public static IAtomUIBuilder UseDesktopControls(this IAtomUIBuilder builder) +public static IAtomUIBuilder UseAcmeControls(this IAtomUIBuilder builder) { - // existing registration order + // 保持 Package Core 和 full/generated 分支顺序。 return builder; } - -builder.UseDesktopControls(); ``` -显式全量入口为: +Attribute 不接收 Package ID、类型名或方法名。Generator 从 `IMethodSymbol` 派生稳定 identity,并验证方法是 public、static、 +非泛型 `IAtomUIBuilder` 扩展方法。项目文件不得维护入口类型名或方法名字符串。 -```csharp -[ControlPackageRegistrationEntry] -public static IAtomUIBuilder UseAllDesktopControls(this IAtomUIBuilder builder) -{ - // explicit full registration - return builder; -} +`UseXxxControls()` 仍拥有 Package Core、Provider、Localization 和 initializer 顺序。应用静态发现不能绕过入口自动启用可选包。 -builder.UseAllDesktopControls(); -``` +隐藏运行时 ABI 保持明确的 `AotTrim` 命名: -`UseDesktopControls()` 只在 `AotTrimRegistration.IsEnabled` 上分支。普通路径调用当前 full helper;生成式路径保留 Package Core -的原有顺序,仅把 package theme 提交替换为 `AotTrimRegistrationPlanRegistry.ApplyPackage(...)`。 +| 类型 | 职责 | +| --- | --- | +| `AotTrimRegistration` | 读取发布 feature switch | +| `AotTrimControlPackageRegistrationBuilder` | 收集已选 Unit 并提交完整 Package registration | +| `AotTrimRegistrationPlan` | 应用静态计划的跨程序集调用协议 | +| `AotTrimRegistrationPlanRegistry` | 安装唯一 plan 并按 Package 静态分派 | -DataGrid、ColorPicker、Extras 和第三方包仍由各自 `UseXxxControls()` 入口触发。静态发现不能绕过用户入口自动注册可选包。 -同一应用进程中的多个 Builder 可以复用同一不可变计划,但每次应用都创建独立 package registration 实例。 +运行时 ABI、fragment 和 Sidecar protocol 都必须有快照和兼容性测试。 -入口声明与入口执行是两个不同阶段:Package Generator 根据 Attribute 生成 entry manifest;应用 Generator 根据真实调用符号 -匹配该 manifest 并生成 Usage。入口方法体仍负责保持 Package Core 顺序以及 full/generated 分支,Generator 不解析方法体 -来猜测入口身份。 +## 10. 第三方 Package -`AotTrimRegistrationPlanRegistry`、Unit fragment 和 manifest ABI 虽然对用户隐藏,仍是跨程序集 Public API,必须有生成源码 -ABI 快照和协议测试。运行时 ABI 的命名必须保持 AOT/Trim 语义,不能退化为 `RuntimeFeatures`、`GeneratedFeatures` 等通用名称。 +普通第三方包只需要: -## 13. 第三方包 - -普通第三方包默认使用 `Package` 粒度。作者只需要: - -1. 引用兼容版本的 AtomUI 产品包,由产品包自动提供同版本 Generator 和构建资产。 -2. 声明稳定的 `AtomUIRegistrationPackageId`。 +1. 引用 AtomUI 产品 Package,让它自动提供 Generator、Build Tasks 和 buildTransitive assets。 +2. 声明稳定 `AtomUIRegistrationPackageId`。 3. 按 Control、可选 Own Token 和 `Themes/` 约定组织源码。 -4. 在真实 `UseXxxControls()` 方法上声明 `[ControlPackageRegistrationEntry]`,不维护方法 metadata 字符串。 -5. 在入口中保持普通 full 注册、generated plan、Provider、Localization 和 initializer 的既有顺序。 -6. 验证普通 full 注册与生成式注册的行为快照。 +4. 在真实 `UseXxxControls()` 上添加 `[ControlPackageRegistrationEntry]`。 +5. 保持 full/generated、Provider、Localization 和 initializer 顺序。 +6. 使用默认 Package 粒度并验证 ordinary/generated 行为。 -普通第三方作者不声明 `AtomUIRegistrationUnit`、Unit dependency、Theme ownership 修补或 linker XML。Generator 输出单一 -Package Unit、ControlMap、隐藏 fragment 和版本化 full registrar;应用仍显式调用 `UseXxxControls()`。 -通常也不单独引用 `AtomUI.Generator`;只有不引用任何 AtomUI 产品包的底层构建项目才显式添加 private Analyzer。 +Pack 自动生成和交付 Sidecar。作者不写 Unit dependency、ownership 修补、Sidecar、linker XML 或运行时扫描。只有大型多控件包 +才启用 Directory 粒度并承担真实发布和体积验证。 -只有大型多控件包才显式设置: +## 11. Trimmability 与打包边界 -```xml -Directory -``` +runtime 项目只有在真实 trimmed JIT 和 NativeAOT 验证后才能声明 `IsTrimmable`、`IsAotCompatible`;warning suppression 不能 +代替兼容实现。 -启用后才需要遵守目录级 owner、PackageShared、依赖诊断和严格 fallback 契约。`AtomUIRegistrationUnit` 只作为 -resource-only Theme 或非常规布局的高级逃生口,不进入普通接入流程。 +ordinary Generator、linked-publish Generator、Build Tasks、Sidecar、PDB 和分析缓存都是纯构建资产: -无法提供兼容 manifest 的旧包可以只提供 full registrar。生成式应用仅对该 Package 执行 full fallback,不运行时扫描旧包。 -面向包作者的完整步骤见 -[第三方 AtomUI Control Package 指南](../../guides/theming/third-party-control-packages.md)。 -Package/Directory 的正式架构规则见 -[AOT Registration Unit 粒度](aot-registration-unit-granularity.md)。 +- 普通构建只注入 ordinary Generator。 +- AOT/Trim 才注入 linked-publish Generator。 +- Pack 自动交付 Sidecar 和唯一 consumer target。 +- 多个产品包必须幂等注入 Analyzer。 +- 构建工具不得进入 `lib/`、runtime dependency graph、应用输出或 publish 目录。 -## 14. Trimmability 与构建边界 +具体 MSBuild 和 NuGet 规则见 [构建与打包](build-and-packaging.md)。 -参与生成式注册的 runtime 项目必须在真实 trimmed JIT 和 NativeAOT 验证后声明 `IsTrimmable`、`IsAotCompatible`。 -这允许 linker 删除未引用 Unit fragment,但不能通过 warning suppression 伪造兼容性。 - -`AtomUI.Generator` 和 `AtomUI.Build.Tasks` 是纯构建期工具,不得进入应用运行依赖。它们必须隔离 `PublishAot`、 -`PublishTrimmed`、`RunAOTCompilation`、RID 和 SelfContained 等 global property,避免工具项目被误判为发布入口。 - -声明 `AtomUIRegistrationPackageId` 的第一方产品 NuGet 必须自动携带同版本 Generator、Build Tasks 和 buildTransitive -资产。用户只引用产品包即可获得应用级计划;显式 Generator PackageReference 仍兼容。多个产品包或显式 Generator -并存时,构建入口必须根据 `ResolveReferences` 后的最终 `@(Analyzer)` 幂等注入,编译器只能接收一份 Generator。 - -`AtomUIRegistrationPackageId` 只表达 Package identity 和产品包 build asset 注入边界,不包含注册方法信息。入口方法只能来自 -`ControlPackageRegistrationEntryAttribute`;不得重新引入任何入口方法 metadata 字符串属性。 - -buildTransitive targets 负责暴露模式属性、生成结构化 AXAML 输入、配置 feature switch,并在 ILLink/ILCompiler 前验证 -应用计划标记。它们的项目求值阶段 Condition 不得读取 item list;需要检查 `@(Analyzer)` 的逻辑必须放入构建 Target -执行阶段。普通构建不得安装运行计划或改变包注册方式。Generator、Build Tasks 及其私有依赖只能存在于 NuGet 的 -构建工具目录,不得进入 runtime dependency graph、`lib/`、应用输出或发布目录。 - -同版本第一方产品包内嵌的 Generator、Build Tasks 和共享 MSBuild 资产必须内容一致。发布打包不得跨版本使用 -`--no-build` 复用陈旧工具输出。 - -## 15. 诊断契约 +## 12. 诊断契约 | ID | 条件 | 默认严重度 | | --- | --- | --- | -| `ATOMUILINK001` | 生成式 publish 缺少或存在多个 Application Plan owner | Error | -| `ATOMUILINK002` | 静态使用无法精确映射,Package 已 full fallback | Warning | -| `ATOMUILINK003` | 第三方包缺少 Unit 协议,使用 full fallback | Warning | +| `ATOMUILINK001` | 缺少或存在多个 Application Plan owner | Error | +| `ATOMUILINK002` | 静态使用无法精确映射,Package full fallback | Warning | +| `ATOMUILINK003` | Package 缺少可验证 Sidecar,full fallback | Warning | | `ATOMUILINK004` | 显式 Unit/Package root 无法解析 | Error | -| `ATOMUILINK005` | Package 粒度值、Unit/Shared 定义冲突或不完整 | Error | -| `ATOMUILINK006` | Manifest 或 Generator ABI major version不兼容 | Error | -| `ATOMUILINK007` | Loose AXAML 或动态主题导致 Package full fallback | Warning | -| `ATOMUILINK008` | 检测到 Package 使用但缺少对应注册入口 | Error | -| `ATOMUILINK009` | Package 注册入口 Attribute 用法、签名、可见性或重载集合无效 | Error | +| `ATOMUILINK005` | Package 粒度、Unit/Shared 或 ownership 冲突 | Error | +| `ATOMUILINK006` | Sidecar 或 Generator ABI major 不兼容 | Error | +| `ATOMUILINK007` | Loose AXAML 或动态主题导致 Package fallback | Warning | +| `ATOMUILINK008` | 检测到 Package 使用但缺少 registration entry | Error | +| `ATOMUILINK009` | Registration entry Attribute 或签名无效 | Error | -可修复诊断必须包含来源位置、Package/Unit identity,以及可直接采用的源码修复或 MSBuild root 示例。不能只输出 -“可能不兼容 AOT”。 -`ATOMUILINK002` 和 `ATOMUILINK007` 的 metadata 传播不依赖当前项目是否 linked;其 Warning 仅在 linked publish 或 -strict 模式可见,strict 模式继续把自动 full fallback 提升为 Error。其他协议错误不因普通构建而静默。 +诊断必须包含 Package/Unit identity、稳定 reason 和可定位输入,不能只写“可能不兼容 AOT”。Fallback Warning 只在 linked publish +或显式 strict 验证中产生;strict 模式可以提升为 Error,但不改变保留范围。 -## 16. 验证与体积门槛 +## 13. 验证和体积门槛 -验证分为: +统一验证必须覆盖: -1. Package Generator 测试:入口 Attribute 符号派生与诊断、Package/Directory 粒度、Unit 归属、同包依赖、PackageShared、unknown 和 full fallback。 -2. Usage 测试:AXAML/C#、类库传播、显式 roots、稳定排序和 diagnostics。 -3. 运行集成测试:full/generated 的 descriptor、asset、语言、initializer、Provider 和冻结时序。 -4. 真实发布测试:trimmed JIT 和 NativeAOT 的构建与运行 smoke;WebAssembly AOT 的生成、编译、链接和优化, - 并在平台基线允许时执行浏览器 runtime smoke。 +- Package/Directory、entry、Unit ownership、direct UnitEdge、PackageShared、budget 和 fallback。 +- Sidecar codec、hash、确定性、ProjectReference/NuGet 传播和 fragment symbol。 +- 普通 Debug/Release 不加载 linked Analyzer、不运行 linked targets、不产生 linked 中间文件。 +- ordinary/generated descriptor、Theme Asset、Language、Provider、initializer 和冻结时序一致。 +- trimmed JIT、NativeAOT、WebAssembly AOT 和真实 Gallery 启动 smoke。 +- Sidecar、Generator、Build Tasks、PDB 和缓存不进入 publish。 -入口声明必须验证:方法重命名自动更新 manifest、多个入口稳定排序、无标记程序集不输出 linked metadata、Common 只走 -完整注册,以及仓库中不存在入口方法 metadata 字符串配置残留。 - -粒度必须验证:普通第三方包零 Unit metadata、ColorPicker 跨目录内部 View、DataGrid 单 Package Unit、Desktop 显式 -Directory、多 Generator 共享同一策略,以及不确定依赖只扩大为 Package full fallback。 - -仓库提供统一验证入口: +仓库统一入口: ```bash scripts/verification/verify-aot-trim-registration.sh --full ``` -该脚本验证 ordinary/generated 行为快照、trimmed JIT、Minimal/TwoUnits/DynamicFallback/Full NativeAOT、未使用 Unit -增量、Browser AOT 以及下列体积门槛。Gallery Desktop 仍需使用发布 workflow 的标准 NativeAOT 命令进行独立启动 smoke, -以覆盖真实窗口、主题资源和首帧布局路径。 +体积比较必须固定 SDK、RID、Configuration、SelfContained、TrimMode 和测量口径。门槛为: -体积比较必须固定 SDK、RID、Configuration、SelfContained、TrimMode 和测量口径。验收门槛为: - -- 最小 Desktop Unit 应用相对 full registrar 至少缩小 `40%`。 -- 新增一个未使用 Unit 后,同一最小应用产物增量不超过 `256 KiB`。 -- 显式 PackageShared、Language 和 initializer 的固定成本在基线报告中单独记录。 +- 最小 Desktop Unit 的 NativeAOT 主程序相对 full registrar 主程序至少缩小 `40%`。 +- 固定 `osx-arm64` Button/Window 样例移除中文字体后,第一阶段主程序不超过 `18 MiB`;后续目标不超过 `16 MiB` + 或同场景 Fluent 的 125%。 +- 新增一个未使用 Unit 后主程序增量不超过 `256 KiB`。 +- ILC map 不保留没有直接或传递证据的其他控件族 Unit。 - full registrar 的非裁剪 Registry 快照无行为差异。 -门槛只能根据可复现的多平台基线调整,并记录 SDK/RID、原始值和调整原因。 +目录总量只统计可交付 payload,排除 `.dSYM`、`.pdb` 和 `.dbg`;缩减率、绝对上限和未使用 Unit 增量统一以主程序 +文件为准,避免调试符号和所有场景共享的 native library 扭曲 linked registration 的收益。门槛只能根据删除缓存后的可复现 +多平台数据调整,并记录 SDK/RID、原始值和原因。 diff --git a/docs/architecture/foundations/aot-linked-registration-pipeline.md b/docs/architecture/foundations/aot-linked-registration-pipeline.md new file mode 100644 index 000000000..98cffb450 --- /dev/null +++ b/docs/architecture/foundations/aot-linked-registration-pipeline.md @@ -0,0 +1,299 @@ +# AOT Linked Registration Pipeline + +> 状态:截至 2026-08-16,本文定义 AtomUI linked registration 的正式架构。 + +本文是 AtomUI linked registration 构建管线、Sidecar Manifest、应用静态计划和运行时边界的正式所有者。 +Registration Unit 的产品粒度和资源归属由 +[AOT Registration Unit 粒度](aot-registration-unit-granularity.md)定义;整体 AOT 目标和 Package Core 由 +[AOT 与裁剪架构](aot-and-trimming.md)定义;Generator 的实现约束见 +[Linked Registration Generator](../../modules/generator/linked-registration.md)。 + +## 1. 架构不变量 + +本架构必须同时保证: + +1. 应用继续只调用真实入口,例如 `builder.UseDesktopControls()`。 +2. 普通第三方包默认使用单一 Package Unit,不要求作者维护 Unit 图、Attribute、linker XML 或额外目录规则。 +3. 普通 Debug 和未启用 AOT/Trim 的 Release 不加载 linked-publish analyzer,不收集 usage,也不生成应用计划。 +4. linked publish 的全部推导在构建期完成;运行时不读取 Manifest、不遍历依赖图、不扫描程序集,也不延迟注册。 +5. Unit fragment 是叶子,只注册本 Unit;应用编译期闭包负责去重和排序。 +6. 无法证明精确结果时只扩大为当前 Package full registrar,不能生成可能漏注册的计划。 +7. 分析规模必须有确定性结构上限;超限只能触发 fallback,不能形成无界运行或数 GB linker 图。 + +静态输入不足时,优先保证作者零新增负担、运行时零分析成本和运行正确性,不承诺所有动态程序都达到理论最小体积。 + +## 2. 构建模式 + +应用级 linked analysis 只由真实 linker 模式启用: + +| 构建模式 | Linked analyzer | Application Plan | +| --- | --- | --- | +| Debug build/run | 不加载 | 不生成 | +| 未裁剪 Release | 不加载 | 不生成 | +| 非裁剪 self-contained / ReadyToRun | 不加载 | 不生成 | +| `PublishTrimmed=true` | 加载 | 入口应用生成 | +| `PublishAot=true` | 加载 | 入口应用生成 | +| `RunAOTCompilation=true` | 加载 | 入口应用生成 | + +NuGet `Pack` 可以用内部 `AtomUIEmitLinkedManifest=true` 加载 Package Manifest compiler,目的是把依赖摘要预计算一次并交付给 +消费者。Pack 不是普通应用构建模式,不能反向让日常 Debug/Release 加载 linked analyzer。 + +`AtomUIUseGeneratedRegistration=true` 不得再隐式加载完整应用分析管线。仓库测试如果需要在非裁剪环境验证 generated path, +必须通过隔离验证 target 显式启用。 + +## 3. 组件与数据流 + +```text +Package source + AXAML + | + | Pack / linked ProjectReference + v +Package Manifest Compiler + | + +--> leaf Unit fragments in assembly + `--> *.atomui-link.json + | + | buildTransitive / ReferencePath companion Sidecar + v +Application AOT/Trim compilation + | + +--> current application C#/AXAML usage + +--> transitive usage sidecars + +--> package declaration/dependency sidecars + v +Compile-time package fallback + Unit closure + stable ordering + v +GeneratedApplicationRegistrationPlan.g.cs + v +Direct static Unit calls +``` + +管线包含三个职责: + +| 组件 | 职责 | +| --- | --- | +| Ordinary Generator | 生成 Theme/Token/Localization、full registrar 和 leaf Unit fragment | +| Package Manifest Compiler | 生成 Package、Unit、ControlMap、UnitEdge、Usage 和 Fallback sidecar | +| Application Plan Generator | 仅在 linked app 中合并 usage、计算闭包并生成最终静态调用 | + +应用编译不重新分析 NuGet Package 源码,ILLink 也不负责推导 AtomUI Unit 关系。 + +## 4. Analyzer 物理隔离 + +仅在 Generator 回调中检查 `AtomUILinkedPublish=false` 不能作为 Debug 零成本证明,因为 Analyzer assembly 仍会被 `csc` 加载。 +Generator 布局为: + +```text +AtomUI.Generator.dll + ordinary generation + +AtomUI.Generator.LinkedPublish.dll + package manifest frontend + application usage analysis + application plan generation +``` + +MSBuild 必须在创建 linked AdditionalFiles、运行 AXAML usage task 和注入 Analyzer 前先判定构建模式。关闭时: + +- `Csc` 的 Analyzer item 中没有 `AtomUI.Generator.LinkedPublish.dll`。 +- 不创建空 usage XML 或 sidecar。 +- 不向 incremental graph 提供 Compilation、SyntaxProvider 或 AdditionalTexts linked input。 +- 不因为项目是 Exe、存在 AXAML 或引用 AtomUI Package 而推断开启。 + +## 5. Sidecar Manifest + +### 5.1 资产边界 + +Sidecar 是纯编译资产: + +- NuGet 中位于 `buildTransitive/AtomUI.LinkedRegistration/`。 +- Package 自己的 `.targets` 只在 linked build 中把它加入 `AdditionalFiles`。 +- linked ProjectReference 在程序集复制到 `TargetPath` 后生成 `.atomui-link.json`;消费项目从已解析 + `ReferencePath` 的 companion 路径收集它,不递归调用 ProjectReference target。 +- `CopyToOutputDirectory`、`CopyToPublishDirectory` 均为 `Never`。 +- 不作为 EmbeddedResource,不进入运行时程序集、应用输出或 publish 目录。 + +### 5.2 记录模型 + +一个 assembly sidecar 可以包含: + +| 记录 | 职责 | +| --- | --- | +| Package | Package ID、entry identities、full/shared fragment、粒度和稳定顺序 | +| Unit | Unit ID、leaf fragment type/method 和 order key | +| ControlMap | CLR metadata name 到 Package/Unit 的唯一 ownership | +| UnitEdge | source Unit 到 target Unit 的直接构建期依赖 | +| Usage | entry、Control Unit、Unit root 或 Package root 使用 | +| Fallback | Package fallback 和稳定 reason code | + +Sidecar 使用 UTF-8、确定性 JSON 和唯一强类型 codec。Writer 统一控制字段顺序、数组排序、转义和路径规范化;协议实现不得 +手工拼接 JSON 字符串。字段级契约由 +[Linked Registration Sidecar](../../reference/aot/linked-registration-sidecar.md)定义。 + +### 5.3 版本和一致性 + +- 未知 major version 必须构建失败。 +- 未识别的 optional minor 字段可以忽略。 +- `contractHash` 覆盖 Package、Unit、ControlMap、fragment identity 和 dependency records。 +- ProjectReference sidecar 与实际引用输出不匹配时必须重建。 +- 首次冷构建必须在 ProjectReference 的 `Build` 返回前生成 Sidecar;不得依赖旧 `TargetPath` 让第二次构建偶然成功。 +- 缺失或陈旧 sidecar 不能被解释为“没有 usage”,只能按兼容规则 full fallback。 +- 发布 sidecar 不包含绝对源码路径;本地编译诊断可以使用真实 Roslyn Location。 + +## 6. Package Manifest 编译 + +### 6.1 Package 粒度 + +默认 Package 粒度只有一个 Unit,不需要 Unit dependency 推导。Manifest compiler 只记录 Package、单一 Unit、ControlMap、 +entry 和 uncertainty。这是普通第三方包的默认正确性边界。 + +### 6.2 Directory 粒度 + +Directory 模式只分析直接构建期证据,不允许从调用点递归进入另一个方法体,也不允许遍历所有 SyntaxTree 的全部 +`DescendantNodes()`。 + +允许的候选包括: + +- Control 类型声明和基类。 +- 显式或隐式对象构造。 +- `typeof(...)` 和可静态解析的 Type 参数。 +- Package registration entry 调用。 +- 同程序集跨 Unit 的直接方法或构造调用。 +- delegate、dynamic、reflection 和已知动态创建 API。 +- 结构化 AXAML 的元素类型、TargetType、BasedOn、selector、template 和 `x:Type`。 + +每个源码/AXAML 文件先由统一粒度策略归入一个 Unit 或 Package Core。直接证据生成紧凑 UnitEdge;传递闭包只在应用编译期 +对 Unit 图计算,不在 Roslyn 方法体或生成的 C# 调用图中展开。 + +Package Core 的普通成员调用不直接生成 Unit root:附加属性访问器和全局服务经常调用 Control 类型上的静态方法,但这不等于 +实例化该控件或需要其主题。Package Core 的显式/隐式构造、`typeof(Control)` 等直接 Type 证据仍生成 Unit root;方法声明的 +返回类型可证明为具体 Control 时,也为该返回 Control 生成 Type 证据。泛型方法在调用点把 `T` 替换为 Control 不视为工厂 +证据,避免 `GetValue()` 等读取路径错误 root Unit。Unit 到 Unit 的直接调用继续生成 `CSharpCall`;应用和普通类库 +调用 Control 类型成员时记录 Control usage,因此 C# 附加属性使用仍能选择对应 Unit。 + +### 6.3 最终输出验证 + +Pack 或 linked ProjectReference 可以在编译后读取 PE metadata 和已编译资源,校验已知 `newobj`、`ldtoken`、entry call 和 +generated resource wrapper 是否都能被 sidecar 解释。验证器不得构造程序集级递归调用图。发现未覆盖证据时,结果降级为 +对应 Package fallback。 + +### 6.4 有界完成 + +分析使用候选数、Unit 数、直接边数和 sidecar bytes 等确定性结构预算,不使用墙钟超时决定输出。预算超限时写入 +`AnalysisBudgetExceeded` fallback;strict 验证可以把该 Warning 提升为 Error。 + +当前硬上限为:单次 C# 或 AXAML 候选 50,000、Registration Unit 2,048、直接 UnitEdge 10,000、ControlMap 50,000、 +Usage 50,000、Fallback 4,096、程序集 Manifest record 64,000、单个 sidecar 4 MiB、当前编译累计 sidecar 32 MiB。 +这些数值属于编译协议实现约束,不能根据机器速度动态变化。 + +超过候选或 sidecar 预算后,分析器不再排序或构造细粒度 Unit 图,只线性保留可证明的 registration entry 和 Package root, +并把受影响 Package 扩大为 full registrar。Sidecar writer 同样把超预算输出压缩为 Package、entry 和 +`AnalysisBudgetExceeded` fallback,不把异常大的精细记录交给消费端。 + +## 7. 传递类库 + +普通类库在日常 Debug/Release 中不扫描 usage,也不生成 Usage Sidecar。Sidecar 只在以下场景自动生成: + +- 被 linked 应用作为 ProjectReference 构建。 +- 自身被 Pack 为 NuGet Package。 + +NuGet Pack 自动交付 usage sidecar 和唯一 consumer target。缺少完整编译期输入的二进制按以下规则处理: + +| 输入 | linked app 行为 | +| --- | --- | +| 完整且可验证的 sidecar | 使用精确 usages | +| 引用 AtomUI controls 但无 usage sidecar | 对相关且已调用入口的 Package fallback | +| 无法确定具体 Package | 对应用已调用的 AtomUI Packages fallback,并报告来源 assembly | + +## 8. 应用静态计划 + +Application Plan Generator 只执行: + +1. 验证 sidecar version、hash、Package ownership 和 fragment symbol。 +2. 汇总当前应用与传递类库的 entry、Control usage、Unit root 和 Package root。 +3. 检查每个已使用 Package 是否显式调用真实 registration entry。 +4. 对 Exact Package 计算 Unit closure 和 SCC。 +5. 对存在 uncertainty 的 Package 选择 full registrar。 +6. 按 dependency SCC 和 Package order key 产生确定顺序。 +7. 生成按 Package ID 分派的强类型静态调用。 + +复杂度接近: + +```text +O(application candidates + sidecar bytes + selected Unit vertices + selected Unit edges) +``` + +应用计划不分析第三方方法体,不遍历 Theme Asset、Catalog、Initializer 或通用 Feature 图。 + +## 9. 叶子 Unit 与运行时边界 + +Unit fragment 只注册本 Unit: + +```csharp +public static void Add(AotTrimControlPackageRegistrationBuilder builder) +{ + builder.AddControl(GeneratedThemeSchemaDescriptorFactory.CreateButton()); + AddThemes(builder); +} +``` + +fragment 中禁止: + +```csharp +builder.TryEnterUnit(...); +OtherUnit.Add(builder); +AddDependencies(builder); +``` + +应用生成代码在编译期完成去重后,每个 leaf fragment 直接调用一次。Unit cycle 先折叠成 SCC,再按稳定 Package 顺序输出, +运行时不需要 visited set 或 dependency graph。 + +Sidecar 不进入运行时。`UseXxxControls()` 仍拥有 Package Core、Provider、Localization 和 initializer 顺序;FeatureSwitch 只在 +普通 full registrar 与应用静态计划之间选择。运行时不得通过异常重试 full registrar。 + +## 10. Fallback + +分析结果只有 `Exact` 和 `PackageFallback`,不存在 BestEffort: + +| 条件 | 行为 | +| --- | --- | +| 精确静态 usage | 选择 Unit closure | +| 默认 Package 粒度 | 选择单一完整 Package Unit | +| dynamic/reflection/loose AXAML | 当前 Package full registrar | +| sidecar 缺失、陈旧或无法绑定 | 当前相关 Package full registrar | +| 未知 protocol major | 构建 Error | +| fragment symbol 不存在 | 构建 Error | +| 分析预算超限 | 当前 Package full registrar | +| usage 存在但 entry 未调用 | 构建 Error | +| Unit cycle | SCC 全选,每个 Unit 调用一次 | + +Warning 必须包含 Package、reason 和可定位的输入身份。不能只报告“可能不兼容 AOT”。 + +## 11. 验证契约 + +### 11.1 普通构建 + +- `Csc` Analyzer item 中不存在 linked analyzer。 +- linked AXAML/sidecar targets skipped。 +- `obj` 中不产生 usage XML、sidecar 或 Application Plan。 +- 与不安装 linked 功能的对照构建相比,耗时与内存差异处于测量噪声内,目标不超过 1%。 + +### 11.2 linked 编译 + +- Generator 代码中不存在全树 `DescendantNodes()` 和递归 member body traversal。 +- no-op rebuild 命中 hash/cache,不重写 sidecar。 +- 异常规模在确定性预算内完成或 Package fallback。 +- Application Plan 只包含选中 Unit 的直接调用。 + +### 11.3 运行与体积 + +- ordinary/generated 的 descriptor、Theme asset、Language、Provider、initializer 和冻结时序一致。 +- trimmed JIT、NativeAOT、WebAssembly AOT 和 Gallery NativeAOT smoke 通过。 +- sidecar、Generator、Build Tasks、PDB 和分析缓存不进入 publish。 +- 固定 `osx-arm64` Button/Window 样例移除中文字体后,第一阶段目标不超过 `18 MiB`,后续目标不超过 + `16 MiB` 或同场景 Fluent 的 125%。 +- ILC map 不得保留没有直接或传递证据的 Picker、ListBox、DatePicker 等 Unit。 + +体积门槛统一使用 NativeAOT 主程序文件。publish payload 报告排除 `.dSYM`、`.pdb` 和 `.dbg`;调试符号以及所有场景 +共享的 native dependency 都不能作为 Registration Unit 保留量的证据。门槛必须基于删除缓存后的真实 publish 和 ILC map; +设计目标不能替代实测结果。 diff --git a/docs/architecture/foundations/aot-registration-unit-granularity.md b/docs/architecture/foundations/aot-registration-unit-granularity.md index edac54d2c..4fb365d15 100644 --- a/docs/architecture/foundations/aot-registration-unit-granularity.md +++ b/docs/architecture/foundations/aot-registration-unit-granularity.md @@ -5,7 +5,8 @@ 本文是 AtomUI Control Package 的 Registration Unit 粒度、资源归属和第三方包接入边界的正式所有者。 整体 linked publish 模式、Application Plan、Package Core、动态 fallback 和 ABI 由 -[AOT 与裁剪架构](aot-and-trimming.md)定义。日常编码规则见 +[AOT 与裁剪架构](aot-and-trimming.md)定义;Sidecar、UnitEdge 和叶子 fragment 管线由 +[AOT Linked Registration Pipeline](aot-linked-registration-pipeline.md)定义。日常编码规则见 [AOT 编程规范](../../engineering/development/aot-programming-guidelines.md),第三方作者的操作步骤见 [第三方 AtomUI Control Package 指南](../../guides/theming/third-party-control-packages.md)。 @@ -85,13 +86,17 @@ Package 模式的目标是让小型包和单一控件族包把正确性建立在 Directory 模式使用稳定控件族目录作为默认 Unit 边界。这里的目录必须代表可独立运行的公开产品能力,例如 Button、 DatePicker 或 Tree,而不能只是 `Cell`、`Utils`、`View`、`Internal` 等代码分类。 +同一历史目录中包含多个独立公开能力时,应把 C# owner 移入各自稳定目录。例如 `Button`、`DropdownButton` 和 +`SplitButton` 分别拥有源码目录;为保持兼容而保留的 `Buttons/Themes/**` 资源 URI 仍由 AXAML `TargetType`/owner 映射回对应 +Unit,不要求作者维护依赖列表或修改公开资源地址。 + 启用 Directory 模式后仍必须满足: - Presenter、Cell、View、Semantic Part 和基础主题跟随其所属公开控件族。 - internal 子目录不能继续自动拆成更小 Unit。 -- Generator 从可以证明的 C# 和 AXAML 使用生成同 Package Unit 依赖。 +- Generator 从可以证明的 C# 和 AXAML 直接证据生成同 Package UnitEdge。 - 作者不手写 Unit dependency 列表。 -- 已知循环依赖由 fragment 的 `TryEnterUnit` 去重,循环不能导致漏注册。 +- 已知循环依赖在应用编译期折叠为 SCC,每个 leaf fragment 只调用一次。 - owner 或依赖无法证明时,当前 Package 必须 full fallback。 Directory 模式是体积优化承诺,不是 AOT 正确性的前置条件。没有真实体积收益、行为测试和发布验证的包不得为了目录整齐 @@ -153,6 +158,10 @@ Package 模式忽略目录拆分,因而不得为内部 Theme 添加这类 meta - Dialog、Tooltip、Motion、Responsive 等 initializer。 - 显式 PackageShared 资源。 +入口直接调用的 initializer、input service 等非 Control helper 必须位于 Package Core 源码边界,不能因为文件恰好放在 +`Dialog`、`Tooltip` 等控件族目录,就把该控件族无条件写成 Root Unit。真正由 Package Core 直接构造或静态使用的 Control +仍按静态证据进入 Unit closure。 + `UseXxxControls()` 仍由包作者维护,因为该方法拥有 Provider、Localization 和 initializer 的执行顺序。Generator 只从 `[ControlPackageRegistrationEntry]` 获取入口身份,并生成 full/generated registration helper;它不解析方法体猜测顺序。 @@ -166,8 +175,8 @@ Package 模式忽略目录拆分,因而不得为内部 Theme 添加这类 meta | --- | --- | | Package 模式中的静态 Control 使用 | 保留完整 Package Unit | | Directory 模式中的静态 Control 使用 | 保留对应控件族 Unit | -| 可证明的同 Package 依赖 | 生成强类型 Unit 直接调用 | -| 已知 Unit 循环 | `TryEnterUnit` 去重并完整保留循环 | +| 可证明的同 Package 依赖 | 写入 Sidecar UnitEdge,由应用编译期计算闭包 | +| 已知 Unit 循环 | SCC 全选,每个 leaf Unit 直接调用一次 | | owner、C#/AXAML 依赖或动态输入无法证明 | 当前 Package full fallback | | manifest major version 不兼容 | 构建错误 | | 检测到 Control 使用但没有调用 Package 入口 | 构建错误 | diff --git a/docs/architecture/foundations/build-and-packaging.md b/docs/architecture/foundations/build-and-packaging.md index d71bb4338..59ae126d1 100644 --- a/docs/architecture/foundations/build-and-packaging.md +++ b/docs/architecture/foundations/build-and-packaging.md @@ -129,7 +129,7 @@ Repository 配置、`MacOSHomebrewNativeAot.targets` 或 `scripts/` 资产。 ## Analyzer 引用方式 -`AtomUI.Generator` 在多个项目中以 Analyzer 形式引用: +ordinary `AtomUI.Generator` 在多个项目中以 Analyzer 形式引用: ```xml ``` -这意味着生成器不是运行时依赖。它在编译期生成 Token 和语言相关代码,运行时依赖的是生成后的类型和资源键。 +这意味着生成器不是运行时依赖。它在编译期生成 Token、Theme、语言和 leaf Registration Unit 代码,运行时依赖的是生成后的 +类型和资源键。 + +linked registration 的应用 usage、Sidecar 和 Application Plan 位于独立的 `AtomUI.Generator.LinkedPublish` Analyzer 中。 +它只在 `PublishTrimmed=true`、`PublishAot=true`、`RunAOTCompilation=true`、Package Manifest Pack 或显式 strict 验证时注入。 +普通 Debug 和未启用 AOT/Trim 的 Release compiler command line 中不得出现该 Analyzer。 ## Linked publish 注册 AtomUI 把 `PublishTrimmed=true`、`PublishAot=true` 和 WebAssembly `RunAOTCompilation=true` 统一视为 linked publish。 -普通非裁剪构建继续使用包级全量注册;linked publish 由 Generator 聚合应用、类库和第三方包的 Usage Manifest, -按 Package 生成 Registration Unit 调用计划。默认一个 Package 生成一个完整 Unit;只有显式设置 +普通非裁剪构建继续使用包级全量注册;linked publish 由 Generator 聚合应用、类库和第三方包的 Sidecar Manifest,计算 +UnitEdge closure 后按 Package 生成静态 Registration Unit 调用计划。默认一个 Package 生成一个完整 Unit;只有显式设置 `AtomUIRegistrationGranularity=Directory` 的大型多控件包才按稳定控件族拆分。Control descriptor、Own Token schema 和 Control-owned AXAML Theme 跟随对应 Unit;Language、包级初始化逻辑、Global Token、Theme Algorithm、Provider、平台 selector 和显式 `PackageShared` 资源作为 Package Core 整体保留。 -完整模式矩阵和注册协议见 [AOT 与裁剪架构](aot-and-trimming.md)。 +完整模式矩阵和注册协议见 [AOT Linked Registration Pipeline](aot-linked-registration-pipeline.md)。 -每个可直接安装、且声明 `AtomUIRegistrationPackageId` 的第一方产品 NuGet 都内嵌同版本的 Generator、Build Tasks 和 -`buildTransitive` assets。应用只引用 `AtomUI.Controls`、`AtomUI.Desktop.Controls`、DataGrid、ColorPicker、Extras 或 +每个可直接安装、且声明 `AtomUIRegistrationPackageId` 的第一方产品 NuGet 都内嵌 ordinary/linked Generator、Build Tasks、 +Sidecar 和 `buildTransitive` assets。应用只引用 `AtomUI.Controls`、`AtomUI.Desktop.Controls`、DataGrid、ColorPicker、Extras 或 GalleryBase 中的实际产品包,即可获得编译期使用分析和 linked registration;不要求额外添加 `AtomUI.Generator` PackageReference。显式 Generator 引用继续作为兼容路径支持。 -产品包入口在 `ResolveReferences` 后检查最终 `@(Analyzer)`,只在尚未存在 `AtomUI.Generator` 时注入同包工具程序集。 -多个产品包同时引用或应用保留显式 Generator 引用时,编译器仍只能收到一份 Generator。共享 props/targets 使用幂等 -property 防止重复导入。Generator 和 Build Tasks 只允许位于包的 `tools/` 与 `buildTransitive/` 目录,不得进入 `lib/` +产品包入口在 `ResolveReferences` 后检查最终 `@(Analyzer)`,只在尚未存在对应 Analyzer 时注入同包工具程序集。 +多个产品包同时引用时,编译器仍只能收到一份 ordinary Generator 和至多一份 linked Generator。共享 props/targets 使用幂等 +property 防止重复导入。Generator、Sidecar 和 Build Tasks 只允许位于包的 `tools/` 与 `buildTransitive/` 目录,不得进入 `lib/` 或应用输出、发布目录。 -这些 `buildTransitive` assets 负责向编译器暴露 `PublishTrimmed`、`PublishAot`、AtomUI 验证开关和 -`@(AvaloniaXaml)` 使用信息,配置 linker 可替换的 `AtomUI.AotTrimRegistration.Enabled` 注册标记,并在 -ILLink/ILCompiler 前验证计划标记。该标记只属于 AOT/Trim 注册基础设施,不得作为通用运行时 feature 使用。 +这些 `buildTransitive` assets 负责在 linked build 中导入 Sidecar、生成结构化 `@(AvaloniaXaml)` usage、配置 linker 可替换的 +`AtomUI.AotTrimRegistration.Enabled` 注册标记,并在 ILLink/ILCompiler 前验证计划标记。普通构建中这些 linked targets 必须 +skip,连空 usage 文件都不能创建。该标记只属于 AOT/Trim 注册基础设施,不得作为通用运行时 feature 使用。 -类库在普通构建中也要生成只含稳定字符串 identity 的 Usage Manifest,因为最终入口应用可能以 linked 模式引用它; -普通构建不得安装 Application Plan、改变 `UseDesktopControls()` 的全量行为或要求动态 root。 +类库只在被 linked 应用作为 ProjectReference 构建或执行 NuGet Pack 时生成 Usage Sidecar。Pack 自动把 Sidecar 和唯一的 +`.targets` 放入 `buildTransitive`;普通 Debug/Release 不扫描 usage,不生成 Sidecar,也不安装 Application Plan。 -Application Plan 不计算 Theme Asset、Catalog、Feature 或 Initializer 图,也不执行应用级依赖闭包。Package 模式天然包含 -包内 Control 依赖;Directory 模式的 Unit 依赖由强类型 fragment 直接表达。无法可靠确定 Unit 时,只对对应 Package 使用 -full fallback。 +Application Plan 只对 Sidecar 中紧凑的 UnitEdge 图计算 closure/SCC,不计算 Theme Asset、Catalog、Feature 或 Initializer 图。 +Unit fragment 是叶子,不调用其他 Unit。无法可靠确定 Unit 时,只对对应 Package 使用 full fallback。 ## 打包边界 diff --git a/docs/architecture/foundations/overview.md b/docs/architecture/foundations/overview.md index 730a577b1..e988d461b 100644 --- a/docs/architecture/foundations/overview.md +++ b/docs/architecture/foundations/overview.md @@ -7,6 +7,8 @@ - [启动与注册链路](startup-and-registration.md):AppBuilder、Application、Builder、Provider 和生成池的注册顺序。 - [构建与打包](build-and-packaging.md):Target Framework、版本、Analyzer、生成输出和 NuGet 包边界。 - [AOT 与裁剪架构](aot-and-trimming.md):linked publish、Application Plan、Package Core、安全 fallback 和体积验证契约。 +- [AOT Linked Registration Pipeline](aot-linked-registration-pipeline.md):Sidecar、Analyzer 激活、UnitEdge closure、叶子 fragment + 和运行时零分析成本。 - [AOT Registration Unit 粒度](aot-registration-unit-granularity.md):Package/Directory 粒度、资源归属、第一方包策略和第三方接入边界。 具体跨模块业务系统进入 `architecture/systems/`;单个项目的源码组织进入 `modules/`。 diff --git a/docs/architecture/foundations/startup-and-registration.md b/docs/architecture/foundations/startup-and-registration.md index 18d3d6f22..6a2386115 100644 --- a/docs/architecture/foundations/startup-and-registration.md +++ b/docs/architecture/foundations/startup-and-registration.md @@ -149,12 +149,13 @@ Popup/Flyout 通过逻辑树自然继承,独立 Window/Dialog/Notification Top DataGrid 和 ColorPicker 独立包通过 `UseDesktopDataGrid()`、`UseDesktopColorPicker()` 追加自己的 Token、主题 Provider 和语言资源。 linked publish 中,应用仍调用 `UseDesktopControls()`,但 `PublishTrimmed=true`、`PublishAot=true` 或 WebAssembly -`RunAOTCompilation=true` 会在编译期切换到生成式 Registration Unit 计划。Desktop 仍完整注册 Common,并保持 +`RunAOTCompilation=true` 会读取纯编译期 Sidecar,在编译期计算 Registration Unit closure 并切换到静态计划。Desktop 仍完整注册 Common,并保持 Dialog input capture、Provider、完整 Language Module 和 Theme initializer 的既有顺序。主 Desktop 包显式使用 `Directory` 粒度,Control descriptor、Own Token schema 和控件族专属 AXAML Theme 按控件族 Unit 保留;DataGrid、 ColorPicker、Extras、GalleryBase 和普通第三方包默认使用单一 Package Unit。无法可靠确定 Unit 时,仅把对应 Package 扩大为 full fallback。普通非裁剪构建继续执行上述全量顺序。完整契约见 -[AOT 与裁剪架构](aot-and-trimming.md)。 +[AOT 与裁剪架构](aot-and-trimming.md)和 +[AOT Linked Registration Pipeline](aot-linked-registration-pipeline.md)。 当前可独立启用的 Package 在真实 `UseXxxControls()` 方法上使用 `[ControlPackageRegistrationEntry]`。Generator 从方法符号生成 entry manifest,不再要求项目文件维护完整类型名和方法名。 @@ -169,8 +170,8 @@ Common 不是独立 linked Package,不声明该 Attribute,也不进入应用 - `ControlThemeAssetManifest.GetDescriptors()`:返回通过构建校验的 ControlTheme asset、owner 和引用 identity descriptor。 - `GeneratedLanguageModuleRegistration.Register()`:显式注册当前项目的 Catalog descriptor 和内置 Translation Bundle。 - `XxxTokens.Identity`、强类型 `XxxTokenKey` 和 `XxxTokenResourceExtension`:供 AXAML 和 C# 使用。 -- linked Package metadata:从带 `[ControlPackageRegistrationEntry]` 的真实 `UseXxxControls()` 方法、Control/Theme 约定和 - 统一 Package 粒度策略生成 Package、Unit、ControlMap 与 fragment 记录;PackageShared 只表达真正的包级共享资源。 +- linked Package Sidecar:从带 `[ControlPackageRegistrationEntry]` 的真实 `UseXxxControls()` 方法、Control/Theme 约定和 + 统一 Package 粒度策略生成 Package、Unit、ControlMap、UnitEdge 与 fragment 记录;PackageShared 只表达真正的包级共享资源。 Builder 必须原样注册包含 exact CLR type 与 identity 的 descriptor 和 manifest,不能退化成只传递其中一项或 运行时扫描 AXAML。因此新增控件 @@ -184,5 +185,8 @@ factory 聚合成一个安全 Registration Unit。只有显式 `Directory` 的 Bundle、Package 初始化逻辑和显式共享资源不拆分。全量池只由普通兼容路径或该 Package 的 full fallback 调用。Theme Builder 最终仍按包接收一个完整且自洽的 `ControlPackageRegistration`,不会改成控件实例化时追加注册。 +每个 Unit fragment 是叶子,不调用其他 Unit。应用静态计划根据 Sidecar UnitEdge 在编译期完成 closure、SCC 和去重; +运行时不读取 Sidecar、不遍历图,也不通过 `TryEnterUnit` 去重。普通 Debug/Release 不加载 linked-publish Analyzer。 + 包作者手写并维护 `UseXxxControls()` 方法体,因为它拥有 Provider、Localization 和 initializer 的执行顺序;Generator 只从 Attribute 读取入口身份,不生成或解析该方法体。项目文件不得维护入口类型名或方法名字符串。 diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index d8e6c4db0..bec3de4da 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -118,4 +118,5 @@ descriptor 和 ControlTheme asset manifest;本地化链路收集 Catalog、编 - linked publish、Registration Unit、Package fallback 和动态 root 遵守 [AOT 与裁剪架构](foundations/aot-and-trimming.md);日常 AOT、反射、 动态数据和生成器规则遵守 [AOT 编程规范](../engineering/development/aot-programming-guidelines.md)。Control Package 的 Package/Directory 粒度和资源归属遵守 - [AOT Registration Unit 粒度](foundations/aot-registration-unit-granularity.md)。 + [AOT Registration Unit 粒度](foundations/aot-registration-unit-granularity.md);Sidecar、Analyzer 激活和静态计划遵守 + [AOT Linked Registration Pipeline](foundations/aot-linked-registration-pipeline.md)。 diff --git a/docs/controls/desktop/general/button/implementation.md b/docs/controls/desktop/general/button/implementation.md index 127423603..ebefb14b0 100644 --- a/docs/controls/desktop/general/button/implementation.md +++ b/docs/controls/desktop/general/button/implementation.md @@ -12,8 +12,8 @@ Button.cs 保留公共属性、事件和接口实现入口;内部 helper 可 主要源码: -- `src/AtomUI.Desktop.Controls/Buttons/Button.cs`:Button public API、Avalonia 属性注册、effective state、伪类同步、CompactSpace / Form / Wave 接口实现。 -- `src/AtomUI.Desktop.Controls/Buttons/ButtonToken.cs`:Button 控件 Token 定义与派生。 +- `src/AtomUI.Desktop.Controls/Button/Button.cs`:Button public API、Avalonia 属性注册、effective state、伪类同步、CompactSpace / Form / Wave 接口实现。 +- `src/AtomUI.Desktop.Controls/Button/ButtonToken.cs`:Button 控件 Token 定义与派生。 - `src/AtomUI.Desktop.Controls/Buttons/Themes/ButtonTheme.axaml`:桌面 Button 模板、状态 selector 和主题变量映射。 - `src/AtomUI.Desktop.Controls/Buttons/Themes/Browser/ButtonTheme.axaml`:Browser Button 模板投影,与桌面主题共享同一 public 状态语义。 - `src/AtomUI.Desktop.Controls/Buttons/Themes/DropdownButtonBaseTheme.axaml`、`DropdownButtonTheme.axaml`:DropdownButton 对 Button 图标尺寸和状态语义的桌面投影。 @@ -22,7 +22,7 @@ Button.cs 保留公共属性、事件和接口实现入口;内部 helper 可 - `src/AtomUI.Desktop.Controls/Buttons/Themes/ButtonTheme.cs`:主题资源注册辅助。 - `src/AtomUI.Controls/Buttons/ButtonPseudoClass.cs`:共享 Button 伪类定义。 - `src/AtomUI.Controls/Buttons/Converters/ButtonIconVisibleConverter.cs`:icon 可见性转换辅助。 -- `src/AtomUI.Desktop.Controls/Buttons/DropdownButton.cs`、`SplitButton.cs`、`IconButton.cs`、`HyperLinkButton.cs`:Button 家族控件。 +- `src/AtomUI.Desktop.Controls/DropdownButton/DropdownButton.cs`、`src/AtomUI.Desktop.Controls/SplitButton/SplitButton.cs`、`src/AtomUI.Desktop.Controls/Button/IconButton.cs`、`src/AtomUI.Desktop.Controls/Button/HyperLinkButton.cs`:Button 家族控件。 ## 3. 核心类职责 diff --git a/docs/controls/desktop/general/split-button/implementation.md b/docs/controls/desktop/general/split-button/implementation.md index 101820b1a..ac42122ec 100644 --- a/docs/controls/desktop/general/split-button/implementation.md +++ b/docs/controls/desktop/general/split-button/implementation.md @@ -10,7 +10,7 @@ 主要源码文件: -- `src/AtomUI.Desktop.Controls/Buttons/SplitButton.cs` +- `src/AtomUI.Desktop.Controls/SplitButton/SplitButton.cs` - `src/AtomUI.Desktop.Controls/Buttons/Themes/SplitButtonTheme.axaml` 职责边界: diff --git a/docs/controls/desktop/navigation/dropdown-button/implementation.md b/docs/controls/desktop/navigation/dropdown-button/implementation.md index c85e2a8e4..1aa9a9cfa 100644 --- a/docs/controls/desktop/navigation/dropdown-button/implementation.md +++ b/docs/controls/desktop/navigation/dropdown-button/implementation.md @@ -10,7 +10,7 @@ 主要源码文件: -- `src/AtomUI.Desktop.Controls/Buttons/DropdownButton.cs` +- `src/AtomUI.Desktop.Controls/DropdownButton/DropdownButton.cs` - `src/AtomUI.Desktop.Controls/Buttons/Themes/DropdownButtonBaseTheme.axaml` - `src/AtomUI.Desktop.Controls/Buttons/Themes/DropdownButtonTheme.axaml` - `src/AtomUI.Desktop.Controls/Buttons/Themes/Browser/DropdownButtonTheme.axaml` diff --git a/docs/engineering/case-studies/avalonia-dynamic-resource-memory-leak-case-study.md b/docs/engineering/case-studies/avalonia-dynamic-resource-memory-leak-case-study.md index 2db9fb871..5cc451c7b 100644 --- a/docs/engineering/case-studies/avalonia-dynamic-resource-memory-leak-case-study.md +++ b/docs/engineering/case-studies/avalonia-dynamic-resource-memory-leak-case-study.md @@ -304,8 +304,8 @@ Flyout 的修复方式: - `src/AtomUI.Desktop.Controls/Flyouts/Flyout.cs` - `src/AtomUI.Desktop.Controls/Flyouts/FlyoutHost.cs` -- `src/AtomUI.Desktop.Controls/Buttons/DropdownButton.cs` -- `src/AtomUI.Desktop.Controls/Buttons/SplitButton.cs` +- `src/AtomUI.Desktop.Controls/DropdownButton/DropdownButton.cs` +- `src/AtomUI.Desktop.Controls/SplitButton/SplitButton.cs` ## 测试范式 diff --git a/docs/engineering/development/aot-programming-guidelines.md b/docs/engineering/development/aot-programming-guidelines.md index 2aeb2f31c..26d33412b 100644 --- a/docs/engineering/development/aot-programming-guidelines.md +++ b/docs/engineering/development/aot-programming-guidelines.md @@ -1,7 +1,8 @@ # AtomUI AOT 编程规范 这份文档给日常写 AtomUI 代码的人用。它不是 AOT 改造记录,而是以后新增控件、主题、图标、语言资源、Gallery 示例和发布配置时要遵守的规则。linked publish 的系统架构、模式矩阵、Registration Unit 和安全 fallback 由 -[AOT 与裁剪架构](../../architecture/foundations/aot-and-trimming.md)统一定义,本文不复制其长期契约。 +[AOT 与裁剪架构](../../architecture/foundations/aot-and-trimming.md)统一定义,Sidecar 和静态计划由 +[AOT Linked Registration Pipeline](../../architecture/foundations/aot-linked-registration-pipeline.md)定义,本文不复制其长期契约。 目标很简单: @@ -229,6 +230,9 @@ Source Generator、Localization writer、Linked Registration metadata 和 AXAML 计划。Control descriptor、Own Token schema、内部控件和控件族专属 Theme Asset factory 必须能聚合为完整 Unit;不得新增 全包静态数组、全资产 `switch` 或“构造全集后过滤”的 linked 路径。 +linked analysis 必须位于独立 Analyzer assembly。普通 Debug 和未启用 AOT/Trim 的 Release 不得把该 Analyzer 传给 `csc`, +也不得运行 AXAML usage、Sidecar 或 Application Plan target。仅在 Generator callback 中快速 return 不算满足零成本要求。 + Control Package 默认使用 Package 粒度: ```xml @@ -268,10 +272,17 @@ AXAML、动态插件等编译期无法确定的场景,不能成为普通 Contr Package 扩大为 full fallback 并给出诊断,不能依赖运行时反射或 late registration 修补。 Directory 模式的 Package Theme 如果直接实例化另一个 AtomUI Control,该元素必须能通过 `using:`、 -`clr-namespace:` 或当前程序集 `XmlnsDefinition` 精确解析,Generator 会生成同 Package Unit 的直接依赖。C# 中可证明的 -跨 Unit Control 使用也必须形成直接依赖。不要依赖短类型名猜测,也不要把模板元素依赖写进 Theme Asset descriptor 的 +`clr-namespace:` 或当前程序集 `XmlnsDefinition` 精确解析,Generator 会把同 Package Unit 的直接依赖写入 Sidecar UnitEdge。 +C# 中可证明的跨 Unit Control 使用也必须形成直接 UnitEdge。不要依赖短类型名猜测,也不要把模板元素依赖写进 Theme Asset descriptor 的 referenced identities;后者会改变普通非裁剪 schema 和 fingerprint。无法证明 owner 或依赖时必须 full fallback。 +Unit fragment 必须是叶子,只注册本 Unit 的 descriptor、Theme asset 和 resource factory。禁止生成 `AddDependencies`、调用 +其他 Unit 或调用 `TryEnterUnit`。应用 Generator 在编译期对 UnitEdge 计算 closure/SCC,并让每个 fragment 最多出现一次。 + +依赖分析使用候选驱动 Incremental API。禁止对所有 SyntaxTree 执行 `DescendantNodes()`,禁止从 invocation 递归进入 callee +body,也禁止为不同 owner Unit 重复扫描相同方法、属性或字段。分析必须有确定性结构预算;超限时当前 Package full fallback, +不能使用墙钟超时产生非确定输出。 + ControlMap 是 CLR Control ownership,不是 descriptor 清单。定义程序集里的 public、非泛型 Control 即使没有 Theme descriptor,也要归入 Registration Unit 并拥有 ControlMap;只有原本可主题化的 Control 才能进入 `builder.AddControl`。 引用程序集不得重复输出 ControlMap。纯基础设施程序集没有 `[ControlPackageRegistrationEntry]` 时,不应生成 @@ -280,13 +291,14 @@ Package、Unit、ControlMap 或 full fragment metadata。 `AtomUI.Controls` Common 层始终由 Desktop 完整注册,不是独立 linked Package。不要为 `UseCommonControls()` 添加入口 Attribute,不要为了让 Common 参与应用计划而复制 `UseDesktopControls()` 的方法 identity,也不要引入跨 Package Unit 闭包。 -类库在普通构建中仍必须生成 PackageRoot fallback metadata,供最终 linked 应用聚合;不要用当前项目未开启 trim/AOT -作为跳过 metadata 的条件。`ATOMUILINK002` 和 `ATOMUILINK007` 只在 linked publish 或 -`AtomUIRegistrationStrict=true` 时显示,普通非裁剪构建不得因这些发布期 fallback 产生 warning。strict 模式仍用于在 -CI 中把自动 full fallback 提升为 error。 +类库只在被 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。 -修改 Generator ABI、Manifest schema、feature switch 或 Public fragment entry point 时,按 Public API 和版本化协议 -review,并运行 trimmed JIT、NativeAOT 和非裁剪兼容验证。 +修改 Generator ABI、Sidecar schema、feature switch 或 Public fragment entry point 时,按 Public API 和协议 review,并运行 +普通构建零 linked-analysis、trimmed JIT、NativeAOT 和非裁剪兼容验证。系统契约见 +[AOT Linked Registration Pipeline](../../architecture/foundations/aot-linked-registration-pipeline.md)。 只有 Directory 模式的内部 resource-only Theme 无法按目录推导到正确 Unit 时,才使用 `AtomUIRegistrationUnit` metadata 明确归属,不要把它升级为 `AtomUIPackageSharedTheme`。Package 模式不得添加这类 ownership 修补。抽象 typed @@ -326,7 +338,7 @@ AppContext key 则必须保留 `AtomUI` 前缀,因为它是进程级字符串 `GeneratedApplicationRegistrationPlan`、`Generated*UnitFragment` 等 `Generated*` 名称仅限 Generator 生成的内部输出, 不要把它们作为运行时 ABI 的通用命名。Generator 内部协议类型可以使用 `LinkedRegistration*`,Build Task 使用 -`CollectAxamlUsageTask`、`ValidateAssemblyMetadataMarkerTask` 这类动作导向名称。 +`CollectAxamlUsageTask`、`WriteLinkedRegistrationSidecarTask`、`ValidateApplicationRegistrationPlanTask` 这类动作导向名称。 ## Theme / Token @@ -608,7 +620,9 @@ observable.ToProperty(...); - `PublishSingleFile=true`:启用 single-file analyzer。 - 显式传入 `EnableTrimAnalyzer`、`EnableAotAnalyzer` 或 `EnableSingleFileAnalyzer` 时,保留调用方选择,用于专项验证。 -普通 Control Package 构建仍需生成 Registration Unit、ControlMap 和 Usage metadata,供以后消费该包的 AOT/trim 应用使用;这是包的编译期静态契约,不等于运行 SDK linker analyzer。应用级 Registration Plan 只能在 `AtomUILinkedPublish=true` 时生成。 +ordinary Generator 可以继续生成普通构建所需的 Theme/Token、full registrar 和 leaf Unit fragment,但不能运行应用 usage 分析。 +Package/Usage Sidecar 只在 NuGet Pack 或 linked ProjectReference 构建时生成;Application Plan 只能在真实 AOT/Trim linked build +中生成。普通构建必须从 `Csc` Analyzer item、AXAML target 和 `obj` 中同时看不到 linked analysis。 ### Analyzer 和真实 publish 都要跑 diff --git a/docs/guides/theming/third-party-control-packages.md b/docs/guides/theming/third-party-control-packages.md index dceb27364..a05ebfbae 100644 --- a/docs/guides/theming/third-party-control-packages.md +++ b/docs/guides/theming/third-party-control-packages.md @@ -1,7 +1,7 @@ # 第三方 AtomUI Control Package 指南 -> 状态:截至 2026-08-15,本文步骤对应已实现并验证的接入方式。普通第三方包默认使用 `Package` 粒度;只有明确承担发布 -> 体积与回归验证的大型包才需要显式启用 `Directory`。 +> 状态:截至 2026-08-16,本文定义第三方 Control Package 的正式接入方式。普通第三方包默认使用 `Package` 粒度;只有 +> 明确承担发布体积与回归验证的大型包才需要显式启用 `Directory`。 本文面向准备发布 AtomUI Control NuGet 的作者。目标是让你的控件同时支持普通应用、trimming、NativeAOT 和 WebAssembly AOT,而不需要维护 linker XML、运行时反射或内部 Registration Unit 图。 @@ -23,6 +23,9 @@ Control descriptor、Own Token、内部 View/Presenter 和主题资源。 应用仍显式调用 `UseAcmeControls()`。Generator 只决定 linked publish 需要保留哪些静态内容,不会运行时扫描程序集,也不会 擅自启用一个可选包。 +普通 Debug 和未启用 AOT/Trim 的 Release 不运行应用 usage 分析。包在 NuGet Pack 时自动生成纯编译期 Sidecar;应用只在 +Trimmed、NativeAOT 或 WebAssembly AOT 发布时读取它。作者不创建、不编辑也不发布运行时 Manifest。 + ## 最小目录结构 一个只提供 `Rating` 的包可以这样组织: @@ -127,7 +130,7 @@ internal sealed class RatingToken : AbstractControlDesignToken ``` Generator 会从真实 CLR 类型、Token 和 AXAML 生成 identity、descriptor、资源键、Theme Asset manifest、full registrar 和 -linked registration metadata。不要为每个 Theme 再写一份 C# 注册代码。 +leaf Registration Unit fragment。NuGet Pack 自动生成对应 Sidecar,不要为每个 Theme 再写一份 C# 注册代码。 ## 第三步:提供 Theme Provider @@ -223,8 +226,8 @@ this.UseAtomUI(builder => ``` -普通构建会执行完整包级注册。Trimmed、NativeAOT 或 WebAssembly AOT 构建会由应用 Generator 生成静态调用计划,但应用代码 -和入口调用不需要切换。 +普通构建会执行完整包级注册,并且不加载 linked-publish Analyzer。Trimmed、NativeAOT 或 WebAssembly AOT 构建会由应用 +Generator 根据 Sidecar 生成静态调用计划,但应用代码和入口调用不需要切换。 ## 普通作者不需要做什么 @@ -240,6 +243,7 @@ Unit dependency 列表 linker XML 运行时 Assembly.GetTypes() 扫描 手工 Control descriptor 或 Theme manifest +手工 Sidecar ``` 其中 `AtomUIRegistrationUnitRoot` 和 `AtomUIPackageRoot` 是消费应用处理真正动态输入的高级开关,不是 Control Package 的正常 @@ -259,7 +263,7 @@ linker XML - 每个目录代表真正可以独立运行的公开控件族,而不只是 `Cell`、`Utils`、`View` 等内部代码分类。 - 内部 Presenter、Cell、View、Track 和主题跟随其公开控件族。 -- AXAML 和 C# 中的跨 Unit 使用能够被 Generator 证明;不确定性必须允许 Package full fallback。 +- AXAML 和 C# 中的跨 Unit 直接证据能够生成 Sidecar UnitEdge;不确定性必须允许 Package full fallback。 - 只有无法从 public owner 推导的 resource-only Theme 才使用 `AtomUIRegistrationUnit`。 - 只有真正跨多个 Unit 且必须随 Package Core 加载的资源才使用 `AtomUIPackageSharedTheme`。 - CI 覆盖 generated registration、trimmed JIT、NativeAOT 和体积对比。 @@ -273,6 +277,7 @@ Directory 模式是高级体积优化,不是 AOT 正确性的前置条件。 - 普通构建调用 `UseAcmeControls()` 后,所有 Control、Token 和主题正常。 - generated registration 模式的 descriptor、Theme Asset、Provider、Localization 和 initializer 与普通模式一致。 - 包内 Control 在 C# 中创建的内部 View/Presenter 仍有完整主题。 +- NuGet 包自动包含编译期 Sidecar 和 consumer target,应用 publish 目录不包含 Sidecar。 - Generator 和 Build Tasks 没有进入应用 runtime dependency、普通输出或 publish 目录。 - 声明 AOT 兼容前,真实 trimmed JIT 和 NativeAOT 应用可以启动并显示控件。 - `git diff --check` 和包自身测试通过。 @@ -280,4 +285,5 @@ Directory 模式是高级体积优化,不是 AOT 正确性的前置条件。 系统级模式、fallback 和诊断契约见 [AOT 与裁剪架构](../../architecture/foundations/aot-and-trimming.md)。Token 与主题作者规则见 [Control Token 设计规范](../../engineering/development/control-token-guidelines.md)。Package/Directory 粒度和资源归属的正式契约见 -[AOT Registration Unit 粒度](../../architecture/foundations/aot-registration-unit-granularity.md)。 +[AOT Registration Unit 粒度](../../architecture/foundations/aot-registration-unit-granularity.md)。Sidecar 和静态计划见 +[AOT Linked Registration Pipeline](../../architecture/foundations/aot-linked-registration-pipeline.md)。 diff --git a/docs/modules/generator/linked-registration.md b/docs/modules/generator/linked-registration.md new file mode 100644 index 000000000..349b1b1fe --- /dev/null +++ b/docs/modules/generator/linked-registration.md @@ -0,0 +1,227 @@ +# Linked Registration Generator + +> 状态:截至 2026-08-16,本文定义 Generator 模块的正式实现边界。 + +本文负责 `AtomUI.Generator`、目标 linked-publish Generator 和 `AtomUI.Build.Tasks` 在 AOT/Trim 注册管线中的职责划分、 +增量模型、禁止模式、输出和测试契约。系统级协议由 +[AOT Linked Registration Pipeline](../../architecture/foundations/aot-linked-registration-pipeline.md)所有,机器可读字段由 +[Linked Registration Sidecar](../../reference/aot/linked-registration-sidecar.md)定义。 + +## 1. 模块职责 + +linked registration 工具链只负责构建期事实: + +- 从真实 registration entry symbol 派生 Package 入口身份。 +- 从统一粒度策略派生 Package/Directory Unit 和 ControlMap。 +- 从 C#/AXAML 直接证据生成 UnitEdge、Usage 和 Fallback。 +- 生成确定性 sidecar。 +- 在入口应用 linked 编译时生成最终静态 Package plan。 +- 校验协议、fragment symbol、sidecar hash 和安全 fallback。 + +它不拥有 Package Core 执行顺序,不解析 `UseXxxControls()` 方法体猜测初始化语义,也不在运行时执行发现。 + +## 2. 组件边界 + +### 2.1 Ordinary Generator + +普通 `AtomUI.Generator.dll` 保留所有日常构建必需工作: + +- Theme Schema、Token、Localization 和 resource wrapper。 +- full registrar。 +- 不包含依赖调用的 leaf Unit fragment。 +- Package entry Attribute 的基本签名诊断,以及普通生成所需的 Unit identity。 + +Ordinary Generator 不扫描应用 usage,不计算 Unit closure,不读取 transitive sidecar。 + +### 2.2 Linked-publish Generator + +目标 `AtomUI.Generator.LinkedPublish.dll` 只在 AOT/Trim、Pack manifest 或仓库 strict 验证中由 MSBuild 注入。它包含: + +- Package Manifest compiler frontend。 +- 应用 C#/AXAML usage analyzer。 +- Sidecar catalog/codec。 +- Application Plan generator。 + +普通 Debug/Release 的 compiler command line 中不得出现该 assembly。 + +### 2.3 Build Tasks + +`AtomUI.Build.Tasks` 负责适合 MSBuild/文件阶段的工作: + +- 结构化 AXAML candidate 收集。 +- Sidecar canonical read/write 和 hash 检查。 +- ProjectReference target output 聚合。 +- Pack asset 与 consumer target 生成。 +- 最终 PE/resource direct-evidence 验证。 + +Build Task 不加载应用运行时,不执行程序集反射,也不构造递归方法调用图。 + +## 3. 增量输入模型 + +linked Generator 使用候选驱动的 Incremental API。允许的源候选为: + +- Control 类型声明和基类。 +- 对象构造、`typeof(...)` 和已知静态 Type 输入。 +- registration entry 调用。 +- Directory 模式的跨 Unit 直接调用。 +- delegate、dynamic、reflection 和已知动态 API。 +- Build Task 输出的结构化 AXAML candidates。 + +每个 transform 输出可比较的 immutable value,例如 metadata name、Unit ID、source identity、reason code 和 location value。 +禁止把 `Compilation`、`SemanticModel`、SyntaxNode、ISymbol 或完整 AdditionalText 内容放入持久缓存结果。 + +`CompilationProvider` 只用于最终 symbol 绑定和输出验证,不能作为“遍历整个 Compilation”的入口。 + +## 4. 禁止的分析模式 + +linked registration 实现中禁止: + +- 对每个 SyntaxTree 调用 `GetRoot().DescendantNodes()`。 +- 从 invocation 递归进入 callee syntax,再从 callee 继续递归。 +- 为每个 owner Unit 重复扫描同一个方法、属性或字段初始化器。 +- 通过方法名字符串或源码文本猜测 registration entry。 +- 把 field/property/parameter/return type 的普通类型引用当成 Control 实例化。 +- 用墙钟超时决定 Manifest 内容。 +- 把不确定性静默忽略或当成没有 usage。 + +需要传递关系时,只能把候选阶段产生的直接 UnitEdge 交给应用计划的紧凑 Unit 图闭包。不得把 dependency 重新编码为 Unit +fragment 之间的 C# 调用。 + +## 5. Package 分析 + +### 5.1 Package 粒度 + +Package 粒度输出一个完整 Unit,不运行 dependency analyzer。所有 Control-owned 内容天然属于该 Unit。 + +### 5.2 Directory 粒度 + +每个 source/AXAML file 必须先经同一个 `RegistrationUnitId` 策略归入 Unit 或 Package Core。直接证据转换规则为: + +- Unit A 直接创建或静态引用 Unit B Control:`A -> B`。 +- Unit A 直接调用 Unit B 拥有的成员:保守 `A -> B`,不进入 B body。 +- Package Core 对 Unit B 的直接 Control 类型证据(显式/隐式构造、`typeof`)生成 Package root 到 B。 +- Package Core 调用的方法声明直接返回具体 Control 时,对返回 Control 的 Unit 生成 Package root;普通成员调用和泛型返回类型的调用点替换本身不形成 root。 +- AXAML owner Unit 引用 Unit B:`A -> B`。 + +Package Core 会随 Package 入口无条件执行,因此它对附加属性访问器、静态事件帮助方法等 Control 成员的调用只证明运行时代码依赖, +不证明对应 Control Theme 会被实例化。把这类 `Call` 直接提升为 Unit root 会让全局服务错误保留完整控件族。Unit 内的跨 Unit +调用仍保守生成 `CSharpCall`;Package Core 只有直接 Type 证据或方法声明的具体 Control 返回值证据才 root Unit。应用和普通 +类库直接调用 Control 类型成员时仍记录该 Control usage,使 C# 附加属性访问可以选择对应 Unit。 + +无法唯一归属、动态 target set 不封闭、generated IL 无法解释或结构预算超限时,输出 PackageFallback。 + +## 6. Sidecar 输出 + +Sidecar writer 必须: + +- 使用唯一强类型 model 和 JSON codec。 +- 对 Package、Unit、ControlMap、UnitEdge、Usage 和 Fallback 稳定排序。 +- 规范化为相对路径,不写入开发机绝对路径。 +- 计算覆盖全部协议事实的 contract hash。 +- 内容未改变时不更新时间戳。 +- 不把 sidecar 添加为 EmbeddedResource、Content 或 publish asset。 + +Codec 的 unknown-major、optional-minor、缺字段、重复 ownership、hash mismatch 和 invalid fragment identity 必须分别测试。 + +## 7. Application Plan 输出 + +Application Plan generator 必须在生成源码前完成: + +1. sidecar catalog 验证。 +2. entry 与 usage 匹配。 +3. Package fallback 决策。 +4. Unit closure 和 SCC。 +5. fragment symbol 验证。 +6. stable package/unit ordering。 + +输出只包含: + +```csharp +GeneratedPackageSharedFragment.Add(packageBuilder); +GeneratedWindowUnit.Add(packageBuilder); +GeneratedTextBoxUnit.Add(packageBuilder); +packageBuilder.Register(); +``` + +每个 Unit fragment 最多出现一次。Generator 不在源码中输出 adjacency array、visited set、dictionary 或运行时 fallback 分支。 + +## 8. Leaf Fragment Writer + +Theme Schema 和 Theme Asset writer 共同生成一个 leaf Unit partial class: + +- Theme Schema 部分添加本 Unit descriptor。 +- Theme Asset 部分添加本 Unit asset 和 resource factory。 +- PackageShared 使用独立 fragment。 +- dependency 只写 sidecar UnitEdge。 + +writer 不生成 `AddDependencies`,不调用其他 Unit,也不调用 `TryEnterUnit`。 + +## 9. 诊断与 fallback + +诊断必须使用稳定 reason code,并包含 Package 和可用 source identity。至少区分: + +- unresolved owner。 +- dynamic/reflection usage。 +- loose or unresolved AXAML。 +- missing/stale sidecar。 +- unknown protocol major。 +- missing fragment symbol。 +- analysis budget exceeded。 +- detected usage without registration entry。 + +可安全扩大的问题使用 PackageFallback Warning;协议不兼容、fragment 不存在和入口缺失使用 Error。strict 模式只提升 fallback +Warning,不改变保留范围。 + +## 10. 性能预算 + +结构预算必须是输入确定的计数,不得依赖机器速度。linked-publish 实现统一使用以下硬上限: + +| 输入或结构 | 上限 | +| --- | ---: | +| C# source candidates | 50,000 | +| AXAML candidates | 50,000 | +| 单个 AXAML usage 输入 | 4 MiB | +| Package count | 1,024 | +| Registration Unit count | 2,048 | +| direct UnitEdge count | 10,000 | +| Package Core root Unit count | 2,048 | +| ControlMap count | 50,000 | +| Usage count | 50,000 | +| Fallback count | 4,096 | +| assembly Manifest records | 64,000 | +| 单个 sidecar | 4 MiB | +| 当前编译累计 sidecar | 32 MiB | + +预算超限立即停止细粒度输出并生成一个 PackageFallback,不继续累积方法、symbol substitution 或 Unit graph。 +应用候选超限后只做线性 entry/Package root 汇总;sidecar 超限后 writer 和 consumer 都只保留生成 full registrar 所需的 +Package 与 entry 信息。SCC 发现和组件依赖排序必须使用显式栈和有界集合,禁止递归 DFS,避免异常长链导致栈溢出。 + +普通构建性能以“linked analyzer 未加载”为验收标准,而不是仅以 callback 快速 return 为标准。linked build 的 no-op rebuild 必须 +复用 sidecar hash 和 Incremental cache。 + +## 11. 测试契约 + +### 11.1 Unit tests + +- Package/Directory 粒度和统一 Unit identity。 +- C# direct candidates、AXAML candidates 和 direct UnitEdge。 +- dynamic、generated output、budget 和 unknown fallback。 +- codec、determinism、hash 和 version compatibility。 +- closure、SCC、stable order 和 fragment symbol validation。 +- leaf fragment 不包含 dependency call 或 `TryEnterUnit`。 + +### 11.2 Build integration + +- Debug/普通 Release 的 `Csc` Analyzer item 不含 linked analyzer。 +- linked AXAML target 在普通构建 skipped。 +- ProjectReference sidecar 只在 linked build 传递。 +- NuGet sidecar/consumer target 自动打包且不进入 runtime assets。 +- sidecar 缺失、损坏或无法验证时只触发 Package fallback。 + +### 11.3 Publish verification + +- ordinary/generated registry snapshot 一致。 +- Minimal、TwoUnits、DynamicFallback、Full 和 unused Unit fixtures。 +- trimmed JIT、NativeAOT、WebAssembly AOT 和真实 Gallery smoke。 +- ILC map 只保留选中 Unit 和可证明依赖。 +- 删除缓存后记录 wall time、peak RSS、主程序大小和 publish asset 清单。 diff --git a/docs/modules/generator/overview.md b/docs/modules/generator/overview.md index 0a1003666..306f8afc6 100644 --- a/docs/modules/generator/overview.md +++ b/docs/modules/generator/overview.md @@ -1,6 +1,8 @@ # AtomUI.Generator 模块概览 -`AtomUI.Generator` 是 AtomUI 的 Roslyn 源生成器项目,TargetFramework 为 `netstandard2.0`。它不作为运行时依赖使用,而是以 Analyzer 方式被多个项目引用。 +`AtomUI.Generator` 是 AtomUI 的普通 Roslyn 源生成器项目,TargetFramework 为 `netstandard2.0`。它不作为运行时依赖使用,而是以 +Analyzer 方式被多个项目引用。AOT/Trim usage、Sidecar 和 Application Plan 位于独立的 linked-publish Analyzer 中,只在对应 +发布构建或 Package Manifest Pack 时加载。 ## 职责 @@ -11,8 +13,9 @@ 包级 manifest;不收集 Global Token 消费白名单。 - 为每个 Control 包生成包级注册 helper 和 manifest,由真实 `UseXxxControls()` 入口调用;不要求逐 Control 或逐 Theme 手工注册。 -- 根据统一 `AtomUIRegistrationGranularity` 策略生成 linked Package、Unit、ControlMap 和 fragment metadata;普通包默认 - 单一 Package Unit,大型包才显式按目录拆分。 +- 根据统一 `AtomUIRegistrationGranularity` 策略生成 Package/Directory Unit identity 和 leaf fragment;普通包默认单一 + Package Unit,大型包才显式按目录拆分。 +- linked-publish Generator 从 C#/AXAML 直接证据生成 Sidecar UnitEdge/Usage,在应用编译期计算 closure/SCC 并输出静态计划。 - 根据 `[LanguageCatalog]` enum 与 XLIFF 2.1 生成强类型资源扩展、Catalog/Bundle 注册和应用 bootstrap。 - 消除 Control 包对 Token、主题资产和语言手工清单及运行时程序集扫描的依赖。 @@ -25,6 +28,7 @@ | `LanguageTagsGenerator` | 从固定语言数据生成常用 BCP 47 `LanguageTags` 和标准 `LanguageDefinition` 元数据 | | `DataMemberAccessorGenerator` | 根据数据模型 Attribute 生成 AOT 友好的数据成员访问器注册 | | `ScopedResourceHostGenerator` | 根据 `[GenerateScopedResourceHost]` 为非 Visual `AvaloniaObject` 生成 scoped 资源宿主生命周期样板代码 | +| Linked-publish generators | 只在 AOT/Trim 或 Package Manifest Pack 中生成 Sidecar、usage 和 Application Plan;不进入普通编译 | ## 关键目录 @@ -34,7 +38,7 @@ | `Localization/` | Catalog symbol、XLIFF、Bundle 编译、模块注册、应用 bootstrap 与 BCP 47 数据生成 | | `DataMemberAccessors/` | 数据成员访问器 Generator、Analyzer 和 SourceWriter | | `ResourceHost/` | 非 Visual `AvaloniaObject` scoped resource host Generator、TypeInfo 和 SourceWriter | -| `LinkedRegistration/` | Package 粒度、入口 manifest、Usage 收集、应用计划、Unit fragment 和安全 fallback | +| `LinkedRegistration/` | Package 粒度、入口 identity、leaf Unit fragment 和共享协议模型 | | `TargetMarkConstants.cs` | 生成器识别的 Attribute 元数据名 | ## 专题文档 @@ -45,6 +49,10 @@ diagnostics、增量生成和 AOT 边界。 - [AOT 与裁剪架构](../../architecture/foundations/aot-and-trimming.md):linked publish、Registration Unit、Application Plan 和安全 fallback。 +- [Linked Registration Generator](linked-registration.md):Analyzer 物理隔离、候选增量分析、Sidecar、leaf fragment、 + Application Plan 和性能预算。 +- [AOT Linked Registration Pipeline](../../architecture/foundations/aot-linked-registration-pipeline.md):Sidecar 资产边界、 + 构建模式、运行时不变量和验证契约。 - [AOT Registration Unit 粒度](../../architecture/foundations/aot-registration-unit-granularity.md):粒度策略、资源归属、 第一方 Package 映射和第三方包契约。 - [第三方 AtomUI Control Package 指南](../../guides/theming/third-party-control-packages.md):包作者的最小项目、入口和验证步骤。 @@ -57,4 +65,7 @@ 目录被 `` 排除且默认被 `.gitignore` 忽略,不应把生成文件当成普通源码维护;只有被结构测试 明确读取的 GalleryBase 快照才需要同步提交。 -新增或修改 Generator 时,应同时检查 writer 代码、诊断规则和生成物稳定性。对于非 Visual `AvaloniaObject` 资源宿主类需求,按 [Scoped Resource Host 开发规范](../../engineering/development/scoped-resource-host.md) 管理 owner 生命周期,并由本模块的 Generator 生成样板代码。 +新增或修改 Generator 时,应同时检查 writer 代码、诊断规则和生成物稳定性。linked registration 不得全树遍历、递归进入 +callee body 或生成 Unit 间调用;普通 Debug/Release 必须从 compiler command line 上完全排除 linked analyzer。对于非 Visual +`AvaloniaObject` 资源宿主类需求,按 [Scoped Resource Host 开发规范](../../engineering/development/scoped-resource-host.md) +管理 owner 生命周期,并由本模块的 Generator 生成样板代码。 diff --git a/docs/overview.md b/docs/overview.md index cdbe7a831..6bf5ace54 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -50,6 +50,7 @@ docs/ - [Native 模块](modules/native/overview.md) - [Windowing 系统](architecture/systems/windowing/overview.md) - [AOT 与裁剪架构](architecture/foundations/aot-and-trimming.md) +- [AOT Linked Registration Pipeline](architecture/foundations/aot-linked-registration-pipeline.md) - [AOT 编程规范](engineering/development/aot-programming-guidelines.md) - [Control 开发规范](engineering/development/control-development-guidelines.md) - [Control 文档规范](engineering/contributing/control-documentation-guidelines.md) diff --git a/docs/reference/aot/linked-registration-sidecar.md b/docs/reference/aot/linked-registration-sidecar.md new file mode 100644 index 000000000..fca7ef2ca --- /dev/null +++ b/docs/reference/aot/linked-registration-sidecar.md @@ -0,0 +1,171 @@ +# Linked Registration Sidecar + +本文定义 AtomUI linked registration Sidecar 的机器可读契约。Sidecar 是构建期资产,不是运行时配置文件。 + +## 文件和编码 + +- 扩展名:`.atomui-link.json`。 +- 编码:UTF-8,无 BOM。 +- 内容:单个 JSON object。 +- NuGet 位置:`buildTransitive/AtomUI.LinkedRegistration/`。 +- ProjectReference 位置:项目 `obj` 输出,由 MSBuild target item 传递。 +- 不作为 `EmbeddedResource`、`Content`、runtime asset 或 publish asset。 + +Producer 必须使用结构化 JSON writer。Consumer 必须使用结构化 parser;不得用正则或字符串切割解析。 + +## 顶层字段 + +| 字段 | 必需 | 含义 | +| --- | --- | --- | +| `protocolMajor` | 是 | 协议 major;未知值构建失败 | +| `protocolMinor` | 是 | 协议 minor;未知 optional 字段可以忽略 | +| `producer` | 是 | 生成工具身份,只用于诊断 | +| `assembly` | 是 | 当前 assembly identity 和 contract hash | +| `packages` | 是 | 当前 assembly 定义的 Package records;可以为空 | +| `usages` | 是 | 当前 assembly 对 AtomUI Package/Unit/entry 的 usage records;可以为空 | +| `fallbacks` | 是 | 当前 assembly 已知的不确定性;可以为空 | + +数组即使为空也必须存在,避免“字段缺失”和“没有记录”产生歧义。 + +## Assembly + +`assembly` 包含: + +| 字段 | 含义 | +| --- | --- | +| `name` | Assembly simple name | +| `contractHash` | 覆盖本文件全部协议事实的 canonical content hash | +| `targetFramework` | 产生 Sidecar 的目标框架 identity | + +Sidecar 与实际引用 assembly identity、target framework 或 hash 不一致时不得消费。ProjectReference 应重新生成;无法重新生成的 +NuGet/二进制输入对相关 Package 使用 full fallback。 + +## Package + +一个 Package record 包含: + +| 字段 | 含义 | +| --- | --- | +| `id` | 稳定 `AtomUIRegistrationPackageId` | +| `assemblyName` | 定义 Package 的 assembly | +| `granularity` | `Package` 或 `Directory` | +| `entryMethods` | 从 `[ControlPackageRegistrationEntry]` 方法符号派生的 metadata names | +| `fullFragment` | full registrar 的 type/method identity | +| `sharedFragment` | 可选 PackageShared fragment identity | +| `units` | 当前 Package 的 Unit records | +| `unitEdges` | 当前 Package 的 direct UnitEdge records | +| `rootUnits` | Package Core 始终需要的 Unit IDs | + +同一 Package ID 只能由一个 assembly 定义。重复 Package、重复 Unit、跨 Package UnitEdge 或无对应 Unit 的 ControlMap 均为协议错误。 + +## Unit + +一个 Unit record 包含: + +| 字段 | 含义 | +| --- | --- | +| `id` | Package 内稳定 Unit ID | +| `fragmentType` | leaf fragment CLR metadata name | +| `fragmentMethod` | leaf fragment static method name | +| `orderKey` | Package generator 产生的稳定注册顺序 | +| `controls` | 当前 Unit 拥有的 public Control metadata names | + +Fragment method 必须可由入口应用 Compilation 解析,并具有生成 ABI 要求的静态签名。Fragment 不得调用其他 Unit、 +`AddDependencies` 或 `TryEnterUnit`。 + +## UnitEdge + +一个 UnitEdge record 包含: + +| 字段 | 含义 | +| --- | --- | +| `sourceUnitId` | 依赖方 Unit | +| `targetUnitId` | 被依赖 Unit | +| `evidenceKind` | `CSharpType`、`CSharpCall`、`AxamlType` 或 `PackageCore` | + +UnitEdge 只表示同 Package 直接依赖。Sidecar 不存储传递闭包;Application Plan Generator 在编译期计算 closure 和 SCC。 +重复 edge 必须去重,自环可以保留或规范化删除,但 producer/consumer 必须使用一致规则。 + +## Usage + +Usage record 的 kind 只允许: + +| Kind | Identity | +| --- | --- | +| `Entry` | Package registration entry method identity | +| `Control` | CLR Control metadata name | +| `UnitRoot` | 完整 Unit ID | +| `PackageRoot` | Package ID | + +Usage 可以带相对 source identity、line 和 column,用于 linked build 诊断。发布 Sidecar 不得包含开发机绝对路径。 + +缺失 Sidecar 不能解释为没有 Usage。无法恢复精确 Usage 时,对可确定的相关 Package执行 full fallback;无法确定 Package 时, +对应用已显式调用的 AtomUI Packages 执行 fallback。 + +## Fallback + +Fallback record 包含: + +| 字段 | 含义 | +| --- | --- | +| `packageId` | 需要 full registrar 的 Package;无法唯一确定时为空 | +| `reason` | 稳定 reason code | +| `source` | 可选相对 source/assembly identity | +| `line` / `column` | 可选位置 | + +至少支持以下 reason: + +- `UnresolvedOwner` +- `DynamicInvocation` +- `ReflectionType` +- `UnresolvedAxaml` +- `LooseAxaml` +- `GeneratedOutputUnverified` +- `MissingSidecar` +- `StaleSidecar` +- `AnalysisBudgetExceeded` + +Fallback 只能扩大保留范围。Consumer 不得忽略未知必需 reason,也不得把 fallback 降级为 Exact。 + +## 确定性 + +Canonical writer 必须: + +1. 使用固定字段顺序。 +2. 按 ordinal Package ID、Unit ID、edge tuple、usage tuple 和 fallback tuple 排序。 +3. 使用统一路径分隔符和相对路径。 +4. 对重复 records 去重。 +5. 在 canonical 内容上计算 `contractHash`。 +6. 内容未变化时不重写文件或更新时间戳。 + +同一输入、SDK、Generator 和目标框架连续生成的 Sidecar 必须 byte-for-byte 相同。 + +## 验证和错误 + +以下情况是构建 Error: + +- 未知 `protocolMajor`。 +- 缺少必需字段或字段类型错误。 +- Package/Unit/Control ownership 冲突。 +- UnitEdge 跨 Package 或引用不存在 Unit。 +- Fragment symbol 不存在或签名不兼容。 +- Sidecar 与 ProjectReference assembly identity/hash 不一致且无法重建。 + +以下情况产生 Package fallback Warning;strict 验证可以提升为 Error: + +- 动态、反射或 Loose AXAML 无法解析。 +- 编译输出 direct-evidence 无法由 Sidecar 解释。 +- NuGet/未知二进制缺少 Sidecar。 +- 确定性分析预算超限。 + +运行时不得读取、验证或恢复 Sidecar 错误。 + +## 资产验证 + +NuGet 和真实 publish 测试必须确认: + +- Package 中 Sidecar 与 consumer target 存在且路径正确。 +- linked build 可以通过 AdditionalFiles 读取 Sidecar。 +- 普通 Debug/Release 不把 Sidecar加入 compiler input。 +- Sidecar 不出现在 `lib/`、runtime assets、应用输出、`.app` bundle 或 publish 目录。 +- Generator、Build Tasks、PDB 和分析缓存同样不进入运行时产物。 diff --git a/docs/reference/aot/overview.md b/docs/reference/aot/overview.md new file mode 100644 index 000000000..d8b0a13f1 --- /dev/null +++ b/docs/reference/aot/overview.md @@ -0,0 +1,7 @@ +# AOT Reference + +本目录定义 AOT/Trim 构建工具和第三方 Package 可以严格依赖的机器可读协议。 + +- [Linked Registration Sidecar](linked-registration-sidecar.md):Sidecar 文件、记录模型、确定性、hash、验证和错误边界。 +- [AOT Linked Registration Pipeline](../../architecture/foundations/aot-linked-registration-pipeline.md):协议在完整构建管线中的位置。 +- [AOT 与裁剪架构](../../architecture/foundations/aot-and-trimming.md):Registration Unit、Package Core 和安全 fallback。 diff --git a/docs/reference/overview.md b/docs/reference/overview.md index 665e93c6c..d3c0406a1 100644 --- a/docs/reference/overview.md +++ b/docs/reference/overview.md @@ -5,3 +5,4 @@ Reference 保存可被工具、第三方包和应用严格依赖的格式、协 - [主题 Reference](theming/overview.md) - [本地化 Reference](localization/overview.md) +- [AOT Reference](aot/overview.md) diff --git a/docs/superpowers/specs/2026-08-16-aot-sidecar-manifest-static-plan-design.md b/docs/superpowers/specs/2026-08-16-aot-sidecar-manifest-static-plan-design.md new file mode 100644 index 000000000..fed0239ca --- /dev/null +++ b/docs/superpowers/specs/2026-08-16-aot-sidecar-manifest-static-plan-design.md @@ -0,0 +1,629 @@ +# AOT Sidecar Manifest 与静态注册计划优化设计 + +> 状态:2026-08-16 提案,待实现与基线验证。 +> +> 本文记录方案选择、迁移路径和验收门槛。实现稳定后,长期契约应同步到 +> [`docs/architecture/foundations/aot-and-trimming.md`](../../architecture/foundations/aot-and-trimming.md)、 +> [`docs/architecture/foundations/aot-registration-unit-granularity.md`](../../architecture/foundations/aot-registration-unit-granularity.md) +> 和 AOT 开发规范;正式架构文档不依赖本文才能成立。 + +## 1. 结论 + +采用“**Package 构建期预计算 Sidecar Manifest + 应用 AOT/Trim 编译期静态计划**”架构,替换当前把 Unit 依赖递归写进 +生成方法的实现。 + +最终路径为: + +1. 普通 Debug 和未启用 AOT/Trim 的 Release 不加载 linked-publish 分析器,不收集 AXAML usage,不扫描 C# usage, + 不创建 SemanticModel,也不生成 Application Plan。 +2. Control Package 只在打包或作为 AOT/Trim `ProjectReference` 构建时,自动生成版本化 sidecar。包作者不写 Attribute、 + Unit 依赖、linker XML、额外目录规则或配置。 +3. 应用只在 `PublishAot=true`、`PublishTrimmed=true` 或 `RunAOTCompilation=true` 时读取 sidecar,并对应用自身源码做候选驱动的 + 增量分析。 +4. Package Unit fragment 不再调用其他 Unit,也不再调用 `TryEnterUnit`。应用生成器在编译期完成 Unit 闭包和去重,输出 + 每个选中 Unit 一次且仅一次的强类型静态调用。 +5. 任何无法证明的 ownership、动态调用、协议缺失或分析预算超限,都只把对应 Package 扩大为 full registrar。禁止生成 + 可能漏注册的“猜测结果”。 + +这套方案同时消除两个同源问题:Generator 不再递归遍历方法体和反复创建 SemanticModel;ILLink 也不再沿 Unit fragment +之间的调用边扩张成巨大的可达方法图。 + +## 2. 背景与证据 + +当前 `RegistrationUnitDependencyAnalyzer` 对 Directory 粒度 Package 执行以下工作: + +- 顺序遍历 Compilation 中的全部 SyntaxTree 和全部 `DescendantNodes()`。 +- 对大量节点请求 `SemanticModel.GetOperation(...)`。 +- 从调用、属性、字段和泛型实参继续进入被引用成员的方法体。 +- 按 Unit 维护访问集合,并把发现的 Control 依赖写成 Unit fragment 之间的递归 `Add(...)` 调用。 + +这使分析成本接近“程序集级调用图遍历”,且生成代码把分析中的保守扩张直接变成 ILLink 静态可达边。一个 Window/TextBox +样例因此能够把 Button、Picker、ListBox、Input primitives 等大量 Unit 一并保留。 + +2026-08-16 在同一台 macOS、同一 SDK、`osx-arm64`、Release、self-contained NativeAOT 条件下,对真实 Button/Window GUI +样例移除中文字体和未使用 ViewLocator 后,主程序基线为: + +| 实现 | 主程序 bytes | MiB | +| --- | ---: | ---: | +| Fluent | 14,723,096 | 14.0410 | +| AtomUI | 28,744,968 | 27.4133 | +| 差值 | 14,021,872 | 13.3723 | + +已排除的主要误判方向: + +- AlibabaSans 字体只贡献 `710,032` bytes,约 `0.6771 MiB`。 +- 最终只保留 15 个 Ant Design Icon,ILC map 中 Icon 节点合计约 `18,999` bytes。 +- 移除 `UseUserThemeDirectory()` 后主程序大小没有变化。 + +主要差值集中在 `__managedcode`、`__const` 和 EH table,与过大的托管方法可达闭包一致。字体和 Icon 仍应独立优化,但不是本次 +约 13.37 MiB 差值的根因。 + +## 3. 不可破坏的不变量 + +### 3.1 应用用户零配置 + +应用继续只调用真实产品入口: + +```csharp +builder.UseDesktopControls(); +``` + +不增加 `UseGenerated...`、注册列表、root Attribute、Manifest API 或不同的 AOT 启动方式。静态使用不能自动启用未调用入口的 +可选 Package。 + +### 3.2 包作者零新增学习成本 + +普通第三方 Control Package 继续使用默认 `Package` 粒度。新方案不要求作者新增: + +- Control 或 helper Attribute。 +- Unit、Unit dependency 或 ownership 列表。 +- linker XML、RD.XML 或 `DynamicDependency`。 +- 新的源码目录规则。 +- 运行时注册代码、反射扫描或延迟初始化。 + +现有公开注册入口和 Theme 组织契约不因本方案增加步骤。Sidecar 的生成、打包和导入由 NuGet build assets 自动完成。 + +### 3.3 普通构建零 linked-analysis 成本 + +对应用和普通类库: + +- `dotnet build -c Debug` 不运行 linked-publish 分析。 +- `dotnet build -c Release` 在未启用 AOT/Trim 时也不运行 linked-publish 分析。 +- 不因为 `OutputType=Exe`、安装了 AtomUI Package、开启普通 Roslyn analyzer 或存在 AXAML 而隐式启用。 +- 只有 AOT/Trim 发布和显式的内部验证目标可以启用应用级分析。 + +Control Package 的 NuGet `Pack` 是唯一额外例外:它在包生产阶段预计算 sidecar,把成本从所有消费项目移到一次打包中。 +这不是普通 Debug/Release 编译路径,且不要求作者手工触发额外命令。 + +### 3.4 运行时零新增分析成本 + +“零成本”指本次优化不在运行时新增推导、发现或图处理。运行时不得: + +- 读取或解析 sidecar/Manifest。 +- 遍历 Unit dependency graph。 +- 使用字典查找 Package 或 Unit。 +- 扫描程序集、反射类型或查找 Attribute。 +- 因缺失资源捕获异常后重试 full registrar。 +- 延迟注册、后台注册或增加分析分配。 + +现有 Theme descriptor、资源集合和 `ControlPackageRegistration` 的必要构造属于产品注册本身,不属于推导成本。本方案还会删除 +生成 fragment 对 `TryEnterUnit` 的调用,从 generated path 移除 `_enteredUnits` HashSet 分配。 + +### 3.5 正确性优先且单调扩大 + +分析结果只有两种合法状态: + +- `Exact`:证据足以生成精确静态 Unit plan。 +- `PackageFallback`:证据不完整,调用该 Package 的 full registrar。 + +不存在 `BestEffort`。任何不确定性只能增加保留内容,不能减少运行所需内容。 + +## 4. 能保证什么,不能保证什么 + +对于包含任意反射、运行时脚本、动态程序集、未知 Source Generator 或插件输入的程序,不可能同时保证: + +1. 理论最小发布体积。 +2. 包作者完全不声明动态边界。 +3. 运行时完全不做发现。 + +本方案选择保证第 2、3 项和运行正确性。静态证据充分时得到细粒度裁剪;证据不充分时自动扩大到 Package full registrar。 +因此它能保证“不漏”,但不承诺每一个动态程序都达到理论最小值。 + +## 5. 总体架构 + +```text +Control Package source / AXAML + | + | Pack 或 linked ProjectReference build + v +Package Manifest Compiler + | + +--> leaf Unit fragments in package assembly + | + `--> .atomui-link.json + | + | NuGet buildTransitive / ProjectReference target output + v +Application AOT/Trim compilation + | + +--> application C#/AXAML direct usage candidates + +--> transitive usage sidecars + +--> package declaration/dependency sidecars + v +Compile-time closure + fallback resolver + v +GeneratedApplicationRegistrationPlan.g.cs + | + `--> direct static Unit calls, no runtime graph +``` + +架构分成三个职责独立的组件: + +| 组件 | 运行时机 | 职责 | +| --- | --- | --- | +| Ordinary Generator | 所有需要 Theme 生成的构建 | 生成现有 Theme schema、resource wrapper、full registrar 和 leaf Unit fragment | +| Package Manifest Compiler | Pack 或 linked ProjectReference | 预计算 Package/Unit/ControlMap、Unit edges、usage 和 uncertainty sidecar | +| Application Plan Generator | 仅 AOT/Trim 应用编译 | 合并 sidecar 与当前应用 usage,生成最终静态 plan | + +## 6. 物理隔离 linked-publish 分析器 + +只在回调入口检查 `AtomUILinkedPublish=false` 仍不能提供最强的 Debug 保证,因为包含多个 Generator 的 Analyzer assembly 仍会被 +`csc` 加载并初始化。因此必须物理拆分: + +```text +AtomUI.Generator.dll + ordinary Theme/Token/Localization generation + +AtomUI.Generator.LinkedPublish.dll + package manifest compiler frontend + application usage analyzer + application plan generator +``` + +MSBuild 只在以下条件之一成立时把 `AtomUI.Generator.LinkedPublish.dll` 加入 `@(Analyzer)`: + +```text +PublishAot == true +or PublishTrimmed == true +or RunAOTCompilation == true +or AtomUIEmitLinkedManifest == true # 仅 Pack/仓库验证内部使用 +``` + +`AtomUIUseGeneratedRegistration=true` 不再作为普通构建隐式加载整套分析器的理由。测试若需要 generated path,必须通过隔离的 +验证 target 显式启用,不能污染产品 Debug 构建。 + +`AtomUILinkedPublish` 的最终值仍作为 CompilerVisibleProperty 传给 linked analyzer,但 Build target 必须在创建 AXAML usage、 +收集 AdditionalFiles 和注入 Analyzer 之前先判定开关。关闭时连空的 usage XML 都不创建。 + +## 7. Sidecar Manifest + +### 7.1 资产边界 + +Sidecar 是纯编译资产: + +- NuGet 路径:`buildTransitive/AtomUI.LinkedRegistration/.atomui-link.json`。 +- 通过 Package 自己的 `.targets` 在 linked build 中加入 `AdditionalFiles`。 +- `CopyToOutputDirectory` 和 `CopyToPublishDirectory` 始终为 `Never`。 +- 不作为 EmbeddedResource,不写入运行时程序集,不进入 `.app` 主程序或发布目录。 +- ProjectReference 通过返回 item 的 MSBuild target 传递 `obj/.../*.atomui-link.json`,不复制到源码树。 + +所有 sidecar 使用 UTF-8、确定性 JSON 和唯一 codec。字段顺序、数组排序、路径规范化和转义由 writer 统一负责,禁止各 Generator +手工拼字符串。 + +### 7.2 顶层模型 + +示意模型如下,实际实现使用强类型 record 和结构化 JSON reader/writer: + +```json +{ + "protocol": 2, + "producer": "AtomUI.Generator.LinkedPublish/6.0", + "assembly": { + "name": "AtomUI.Desktop.Controls", + "contractHash": "sha256:..." + }, + "packages": [], + "usages": [], + "fallbacks": [] +} +``` + +一个 assembly sidecar 同时允许包含: + +| 记录 | 内容 | +| --- | --- | +| `Package` | Package ID、entry method identities、full/shared fragment、粒度、稳定顺序 | +| `Unit` | Unit ID、leaf fragment type/method、稳定 order key | +| `ControlMap` | CLR metadata name 到 Package/Unit 的唯一 ownership | +| `UnitEdge` | source Unit 到 target Unit 的直接编译期依赖及证据类型 | +| `Usage` | 当前程序集使用的 entry、Control Unit 或 Package root | +| `Fallback` | Package fallback、原因码和可选的相对来源位置 | + +发布给消费者的 sidecar 不包含绝对源码路径。Package 构建阶段的本地诊断可以包含真实 Location;写入包时只保留相对路径、 +程序集身份或稳定 reason code。 + +### 7.3 版本和绑定 + +- `protocol` major 不兼容时构建失败,不能猜测字段含义。 +- 未识别的 optional minor 字段可以忽略。 +- `contractHash` 覆盖 Package、Unit、ControlMap、fragment identity 和依赖记录,用于发现陈旧 sidecar。 +- ProjectReference sidecar 必须与本次引用输出的 assembly identity/hash 匹配;不匹配时重新生成,不能复用旧缓存。 +- NuGet sidecar 与包内 assembly 版本共同发布,包内 target 只能导入自己的相对路径资产。 + +## 8. Package Manifest 生成算法 + +### 8.1 默认 Package 粒度 + +普通第三方包默认只有一个 Unit,因此根本不需要 Unit dependency 推导。Manifest compiler 只输出 Package、单一 Unit、 +ControlMap、entry 和可能的动态 fallback。 + +这是“作者零负担”和“正确性默认安全”的主要保证。Directory 分析不是普通第三方包的接入前置条件。 + +### 8.2 Directory 粒度的一次性摘要 + +Directory 模式只用于经过体积和运行验证的大型多控件包。分析器不再从一个调用点递归进入另一个方法体,也不再对每棵树执行 +全量 `DescendantNodes()`。 + +使用 Incremental `SyntaxProvider` 只索引以下候选: + +- Control 类型声明和基类。 +- 显式/隐式 `new`。 +- `typeof(...)` 和已知静态 Type 参数。 +- Package registration entry 调用。 +- 同程序集、跨 Unit 的直接方法/构造调用。 +- delegate、dynamic、reflection 和已知动态创建 API。 +- 结构化 AXAML 中的元素类型、TargetType、BasedOn、selector、template 和 `x:Type`。 + +每个源码文件先按现有 Registration Unit 策略自动归入: + +- 一个 Directory Unit;或 +- 始终保留的 Package Core。 + +分析只生成“直接证据”: + +- Unit A 的文件直接创建或引用 Unit B Control,生成 `A -> B`。 +- Unit A 直接调用 Unit B 拥有的成员,保守生成 `A -> B`,但不进入 B 方法体。 +- Package Core 直接依赖 Unit B,生成 Package root `-> B`。 +- AXAML owner Unit 引用 Unit B 类型,生成 `A -> B`。 + +Unit 图的传递闭包只在应用 plan 阶段对紧凑 Unit ID 图计算。它不在 Roslyn syntax 上递归,也不生成 fragment 之间的调用图。 + +### 8.3 不确定性 + +以下情况直接产生 `PackageFallback`: + +- owner 文件不能唯一归入 Unit 或 Package Core。 +- dynamic invocation、反射 Type 名称、运行时脚本或插件输入无法解析。 +- interface/delegate 调用可能跨 Unit,但静态 target 集不能封闭。 +- 其他 Source Generator 产生的最终 IL 使用未被声明或验证。 +- AXAML namespace、loose XAML、动态 ResourceDictionary 或 selector 无法绑定到唯一 Package。 +- 控制图、Unit ownership 或 fragment identity 冲突。 +- 分析结构预算超限。 + +Sidecar 生成后增加一次仅在 Pack/linked ProjectReference 运行的最终程序集验证。验证器读取 PE metadata 和编译资源,检查已知 +`newobj`、`ldtoken`、entry call 和 generated resource wrapper 是否都能被 sidecar 解释。它不构建程序集级递归调用图;发现 +未覆盖证据时降级为 PackageFallback。 + +### 8.4 确定性结构预算 + +禁止用墙钟超时决定输出,因为这会让不同机器产生不同 Manifest。使用确定性结构上限,例如候选数、直接边数、Unit 数和 +Sidecar bytes。超过上限时: + +1. 停止细粒度推导。 +2. 写入 reason code 为 `AnalysisBudgetExceeded` 的 PackageFallback。 +3. linked publish 输出一次明确 Warning;strict 验证将其提升为 Error。 + +这样异常代码规模只会牺牲体积,不会再次形成无限循环、栈溢出或数 GB ILLink 图。 + +正式预算与实现统一为:C#/AXAML candidates 50,000、Unit 2,048、UnitEdge 10,000、ControlMap/Usage 50,000、 +Manifest record 64,000、单 sidecar/AXAML usage 4 MiB、累计 sidecar 32 MiB。应用超限时只线性保留 entry 和 Package root; +Sidecar 超限时只保留 Package、entry 和 `AnalysisBudgetExceeded` fallback。Unit SCC 与组件排序使用完全迭代算法。 + +## 9. 应用 AOT/Trim 消费算法 + +Application Plan Generator 的输入只包含: + +- 当前应用的候选驱动 C#/AXAML usage。 +- ProjectReference 返回的 sidecar。 +- NuGet buildTransitive 导入的 sidecar。 +- 用户已经显式调用的 Package registration entry。 + +处理顺序为: + +1. 验证所有 sidecar major version、hash、Package ownership 和 fragment symbol。 +2. 汇总当前应用和传递类库中的 entry、Control usage、Unit root、Package root。 +3. 检查每个被使用 Package 是否显式调用入口;缺失入口仍为构建错误。 +4. 对 `Exact` Package 从直接 Unit roots 计算 Unit 图闭包。 +5. 对存在任何 fallback 的 Package选择 full registrar。 +6. 把选中 Unit 按 Package 声明顺序和依赖 SCC 的确定顺序排序。 +7. 生成一个最终静态 Application Plan。 + +复杂度接近: + +```text +O(当前应用 usage candidates + sidecar bytes + selected Unit vertices + selected Unit edges) +``` + +应用编译不分析第三方 Package 源码,不遍历第三方方法体,也不让 ILLink 参与依赖推导。 + +## 10. 叶子 Unit 与静态计划 + +### 10.1 Package 输出 + +Unit fragment 只注册自己的 descriptor 和 Theme,不调用 dependency Unit: + +```csharp +public static partial class GeneratedRegistrationUnit_Button_0123 +{ + public static void Add(AotTrimControlPackageRegistrationBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + builder.AddControl(GeneratedThemeSchemaDescriptorFactory.CreateButton()); + AddThemes(builder); + } + + static partial void AddThemes(AotTrimControlPackageRegistrationBuilder builder); +} +``` + +fragment 中禁止出现: + +```csharp +builder.TryEnterUnit(...); +OtherUnit.Add(builder); +AddDependencies(builder); +``` + +Theme Asset Generator 也只贡献本 Unit 的 Theme 内容;它把 AXAML direct edges 写入 sidecar,不再写入 C# 方法调用。 + +### 10.2 应用输出 + +应用编译期闭包完成后直接生成: + +```csharp +var packageBuilder = new AotTrimControlPackageRegistrationBuilder( + builder, + provider, + includeIdentity, + selectAssets); + +GeneratedDesktopPackageSharedFragment.Add(packageBuilder); +GeneratedRegistrationUnit_Window_0123.Add(packageBuilder); +GeneratedRegistrationUnit_TextBox_4567.Add(packageBuilder); +GeneratedRegistrationUnit_Button_89AB.Add(packageBuilder); +packageBuilder.Register(); +``` + +每个 fragment 在源码中最多出现一次。Unit cycle 在编译期先折叠成 SCC,再按稳定 Package order 输出;运行时不需要 visited set。 + +### 10.3 运行时桥接 + +`UseDesktopControls()` 保持现有 FeatureSwitch 分支和静态 Package switch bridge。本方案不新增运行时查找结构。可以把 +`AotTrimRegistrationPlanRegistry` 的 heap record 改为静态 owner/delegate 字段,避免安装计划时的额外 record 分配;这属于独立的 +常量成本清理,不改变本方案的正确性。 + +## 11. 传递类库和未知二进制 + +普通应用类库在 Debug 中不生成 usage metadata。它只在以下时机自动生成 usage sidecar: + +- 被 AOT/Trim 应用作为 ProjectReference 构建。 +- 自身被 `Pack` 为 NuGet 包。 + +NuGet Pack 自动把 usage sidecar 和唯一的 `.targets` 一同打入 `buildTransitive`。作者不维护打包 item。 + +消费到旧版或非 AtomUI-aware 二进制时采用保守策略: + +| 情况 | 行为 | +| --- | --- | +| 完整且可验证的 sidecar | 使用精确 usages | +| assembly 引用 AtomUI Control Package 但没有 usage sidecar | 对该 assembly 可能涉及且已调用入口的 Package full fallback | +| 无法判断具体 Package | 对应用已调用的 AtomUI Package full fallback,并报告来源 assembly | + +缺失 sidecar 不允许被解释为“没有使用 Control”。 + +## 12. MSBuild 激活矩阵 + +| 场景 | Linked Analyzer | AXAML Usage Task | Sidecar 输出 | Application Plan | +| --- | --- | --- | --- | --- | +| Debug build | 不加载 | 不运行 | 无 | 无 | +| 普通 Release build | 不加载 | 不运行 | 无 | 无 | +| Release + analyzer warnings | 不加载 | 不运行 | 无 | 无 | +| `PublishTrimmed=true` | 加载 | 运行 | ProjectReference 自动输出 | Exe/WinExe 输出 | +| `PublishAot=true` | 加载 | 运行 | ProjectReference 自动输出 | Exe/WinExe 输出 | +| `RunAOTCompilation=true` | 加载 | 运行 | ProjectReference 自动输出 | Exe/WinExe 输出 | +| NuGet Pack | manifest 模式加载 | 仅为 sidecar 运行 | 打包 | 无 | +| 仓库 strict 验证 | 显式加载 | 运行 | 验证输出 | 按 target 决定 | + +非 AOT/Trim 的 `dotnet publish` 也不启用。`AtomUILinkedPublish` 必须在 `PrepareForBuild` 前稳定下来,后续 target 不得通过 +`OutputType` 或 Configuration 再推断一次。 + +## 13. 增量和缓存 + +- 每个 SyntaxProvider 输出按单个候选或单个 AXAML 文件建模,使用可比较的 immutable value,不把 `Compilation`、 + `SemanticModel` 或 SyntaxNode 放入缓存值。 +- AXAML Task 使用输入文件 hash、Package/Unit roots hash 和 protocol version 作为增量 key。 +- Sidecar writer 先比较 canonical content hash,内容未变化时不改时间戳。 +- Application Plan 对每个 sidecar 单独解析并缓存,单一 Package 更新不使其他 Package 重新分析。 +- Unit closure 使用整数索引和紧凑 adjacency arrays;禁止在 generated runtime code 中携带这些结构。 +- 禁止在 generator callback 中嵌套调用 `GetSemanticModel` 并重新遍历另一个方法体。 + +## 14. Fallback 与诊断矩阵 + +| 条件 | 输出 | 正确性 | +| --- | --- | --- | +| 精确 Control/AXAML usage | 选中 Unit 闭包 | 精确 | +| Package 默认粒度 | 单一 Package Unit | 保守且完整 | +| dynamic/reflection/loose AXAML | 当前 Package full registrar | 完整,体积扩大 | +| 缺失或陈旧 sidecar | 当前相关 Package full registrar | 完整,体积扩大 | +| 未知 sidecar major | 构建 Error | 禁止不兼容输出 | +| fragment symbol 不存在 | 构建 Error | 禁止产生坏调用 | +| 分析预算超限 | 当前 Package full registrar | 有界完成 | +| 检测到 usage 但未调用 entry | 构建 Error | 保持显式启用契约 | +| Unit cycle | 编译期 SCC 全选,每个 Unit 调用一次 | 完整,无运行时递归 | + +Warning 必须说明“哪个 Package 因为什么原因扩大”,不能只写“可能不兼容 AOT”。strict 模式可把自动 fallback 提升为 Error, +但普通用户不需要理解或修复 Unit graph。 + +## 15. 明确拒绝的方案 + +### 15.1 继续优化当前递归方法分析器 + +即使增加 visited set 或修复某个循环,它仍把 Roslyn 方法图、生成方法图和 ILLink 可达图绑定在一起。性能和体积问题会以新的 +代码形态再次出现。 + +### 15.2 在运行时读取 Manifest + +这会增加启动 I/O、解析、分配、错误分支和 AOT 动态行为,违反运行时零分析成本。 + +### 15.3 要求作者声明 Unit 依赖 + +这是第二份实现事实来源,重构后容易过期,也把本应由工具承担的 AOT 正确性转移给库作者。 + +### 15.4 为每个 Control 增加 Attribute 或 linker XML + +这增加学习成本且不能覆盖 AXAML、generated code、动态 factory 和资源 ownership。 + +### 15.5 让 ILLink 自己推导并修补注册 + +ILLink 只知道可达性,不知道 AtomUI Theme registration 的产品顺序和资源语义。自定义 linker step/IL rewriting 还会绑定 +NativeAOT、trimmed JIT 和 WebAssembly 的内部实现,不适合作为稳定跨平台协议。 + +### 15.6 用静态构造函数自动注册 Control + +这会引入运行时副作用、顺序不确定、额外分配和难以裁剪的全局状态,不满足用户继续只调用 Package 入口的契约。 + +## 16. 迁移计划 + +### Phase 0:冻结事故面 + +- 为当前递归 analyzer 增加确定性结构上限和 Package fallback,先保证不会再次无限扩张。 +- 确认普通 Debug/Release 的 analyzer、AXAML target 和 publish analyzer 开关均为关闭。 +- 固化当前 Minimal、Button/Window、DynamicFallback 和 Full 的 size/map/binlog 基线。 + +### Phase 1:叶子 fragment 与 sidecar + +- 新增强类型 model/codec 和 deterministic writer。 +- Theme Schema/Asset writer 停止生成 Unit dependency 调用和 `TryEnterUnit`。 +- Package manifest compiler 输出 UnitEdge 和 fallback。 + +### Phase 2:静态 Application Plan + +- Application generator 读取 sidecar,在编译期计算 closure/SCC。 +- 生成直接 Unit 调用并验证 fragment symbol。 +- 删除当前 Application Plan 对 recursive Unit fragment 的依赖。 + +### Phase 3:Debug 物理隔离 + +- 拆出 `AtomUI.Generator.LinkedPublish.dll`。 +- 更新 buildTransitive Analyzer 注入和 AXAML target 条件。 +- 增加 binlog/Compiler command line 测试,证明普通构建没有加载 linked analyzer。 + +### Phase 4:传递 sidecar 与 Pack + +- 完成 ProjectReference target output 聚合。 +- Pack 自动生成 usage/package sidecar 和唯一 consumer target。 +- 验证 sidecar 不进入 runtime/publish 资产。 + +### Phase 5:收尾 + +- 删除 AssemblyMetadata dependency graph 和 `TryEnterUnit` 生成调用。 +- 删除只服务递归 Unit fragment 的 analyzer、codec 和测试。 + +每个 Phase 都必须可以独立回滚。未通过正确性矩阵时只能回退到 Package full registrar,不能回退到不完整的细粒度 plan。 + +## 17. 验收矩阵 + +### 17.1 Debug/Release 成本 + +普通 Debug 和非 AOT/Trim Release 必须同时满足: + +- `Csc` 的 Analyzer item 中不存在 `AtomUI.Generator.LinkedPublish.dll`。 +- `CollectAtomUIAxamlUsage` 和 sidecar target 均为 skipped。 +- `obj` 中不产生 linked usage XML、sidecar 或 Application Plan。 +- Generator telemetry 中 linked analyzer 执行次数为 0。 +- 与移除 linked-publish 功能的对照构建相比,中位耗时和峰值内存差异处于测量噪声内;目标不超过 1%。 + +### 17.2 AOT/Trim 编译性能 + +在固定 SDK、RID、机器和冷/热缓存条件下记录 binlog 与 `/usr/bin/time`: + +- 不允许出现全树 `DescendantNodes()` 或递归成员 body traversal。 +- Package manifest 的 no-op rebuild 只做 hash/cache 命中,不重新分析未变文件。 +- AtomUI.Desktop.Controls clean manifest 分析目标不超过总 `CoreCompile` 的 10%,且不超过 2 秒。 +- Application Plan 生成目标不超过 500 ms。 +- linked analyzer 峰值附加内存目标低于 256 MiB。 +- 任意预算超限必须在有界时间内产生 PackageFallback,不得继续增长。 + +阈值需用实现前基线校准;调整必须记录原始数据和理由,不能为了让测试通过静默放宽。 + +### 17.3 运行时正确性 + +- ordinary full registrar 与 generated registrar 的 descriptor、Theme asset、Language、Provider、initializer 和冻结时序快照一致。 +- 每个 Directory Unit 都有隔离 fixture:创建公开 Control、应用默认 Theme、打开 Window 并完成首帧。 +- Window/Button、TextBox、DatePicker、DataGrid、ColorPicker、dynamic fallback 和 PackageShared 场景全部 smoke 通过。 +- Unit cycle fixture 每个 Unit 只调用一次,资源和 descriptor 不缺失。 +- trimmed JIT、macOS/Windows/Linux NativeAOT、WebAssembly AOT 按平台能力验证。 + +### 17.4 发布体积 + +固定 `osx-arm64`、Release、self-contained NativeAOT、相同 SDK 和相同示例源码: + +- Button/Window 主程序移除中文字体后,以 `<= 18 MiB` 为第一阶段硬目标。 +- 以 `<= 16 MiB` 或不超过同场景 Fluent 125% 为后续优化目标;达不到时必须由 ILC map 解释剩余 Package Core 成本。 +- 新增一个未使用 Unit 后主程序增量继续不超过 `256 KiB`。 +- ILC map 中不得因 Window/Button usage 保留无直接/传递证据的 Picker、ListBox、DatePicker 等 Unit fragment。 +- sidecar、Generator、Build Tasks、PDB 和分析缓存不得进入主程序或 publish 目录。 + +体积目标是实现验收门槛,不是仅凭设计即可承诺的结果。任何数字都必须以删除缓存后的真实 publish 和 ILC map 为准。 + +### 17.5 协议和缓存 + +- JSON round-trip、确定性排序、转义、未知 major、minor compatibility 和 hash mismatch 测试通过。 +- 同输入连续生成 sidecar byte-for-byte 相同,第二次不更新时间戳。 +- 改动一个 Unit 只使该 Package sidecar 和最终 Application Plan 失效。 +- 缺失 sidecar、损坏 sidecar和未知二进制均走定义好的 Error 或 full fallback。 + +## 18. 实现影响面 + +预计主要修改所有权如下: + +| 区域 | 改动 | +| --- | --- | +| `src/AtomUI.Generator` | leaf fragment、移除递归 dependency writer、保留 ordinary generation | +| 新 linked generator project | candidate analysis、sidecar reader、Application Plan | +| `src/AtomUI.Build.Tasks` | deterministic sidecar/AXAML/PE validation 和 MSBuild task | +| `build/AtomUI.LinkedRegistration.*` | 严格激活矩阵、ProjectReference/NuGet sidecar 传递 | +| `src/AtomUI.Core/Registration` | generated path 不调用 TryEnter;可选移除 plan entry record 分配 | +| Generator tests | protocol、candidate、fallback、static plan、determinism | +| publish fixtures/scripts | Debug 零成本、真实 AOT、size/map/runtime smoke | + +本方案不要求修改 Control public API、应用启动 API 或 Package registration entry API。若实现中发现必须新增 Public API,必须停止 +并单独评审,不能把临时 ABI 混入本次性能修复。 + +## 19. 风险与回滚 + +| 风险 | 控制 | +| --- | --- | +| Sidecar 与 assembly 陈旧 | contract hash + symbol validation;不匹配时重建或 full fallback | +| NuGet transitive asset 丢失 | pack contract test + consumer fixture;缺失不解释为空 usage | +| Directory direct edge 仍过宽 | ILC map Unit 归因;优化第一方目录/Package Core ownership,不给第三方加规则 | +| Generated code 无法观察 | final assembly direct-evidence verifier;无法解释则 full fallback | +| Sidecar 与 assembly 协议不一致 | major-aware catalog;缺可信 graph 时 Package fallback | +| 注册顺序变化 | stable order key + SCC order + ordinary/generated snapshot | +| 分析规模异常 | 确定性结构预算 + PackageFallback,无墙钟非确定性 | + +紧急回滚开关只允许把 Package 切换为 full registrar。不得重新启用 recursive fragment dependency graph,也不得通过关闭 +诊断保留不完整 plan。 + +## 20. 最终判断 + +这是在现有约束下最接近“完美”的可行解: + +- 应用和包作者的使用方式不变。 +- 普通 Debug/Release 从构建图上移除整套 linked analysis。 +- AOT/Trim 的推导成本有界、增量、可缓存,并从方法图缩小为候选摘要和 Unit 图。 +- 运行时没有 Manifest、依赖图、反射或 fallback 逻辑。 +- ILLink 只看到最终选中的叶子 fragment,不再替 Generator 承担错误的依赖闭包。 +- 静态信息不足时明确牺牲体积而不是牺牲正确性。 + +它不依赖“分析器永远足够聪明”的假设,而是把可证明精确、不可证明回退、编译性能上限和真实发布验收同时写进协议。 diff --git a/scripts/verification/verify-aot-trim-registration.sh b/scripts/verification/verify-aot-trim-registration.sh index c755a16f3..a9060838b 100755 --- a/scripts/verification/verify-aot-trim-registration.sh +++ b/scripts/verification/verify-aot-trim-registration.sh @@ -3,6 +3,7 @@ set -euo pipefail MINIMUM_DESKTOP_REDUCTION_PERCENT=40 MAX_SECOND_UNIT_GROWTH_BYTES=262144 +MAX_MINIMAL_MAIN_BYTES_OSX_ARM64=18874368 repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" output_root="${ATOMUI_AOT_TRIM_OUTPUT_ROOT:-/tmp/atomui-aot-trim-registration}" @@ -61,7 +62,15 @@ fixture_project() { restore_fixture() { local fixture="$1" - dotnet restore "$(fixture_project "$fixture")" --disable-build-servers + local linked="${2:-false}" + local restore_args=( + restore "$(fixture_project "$fixture")" + --disable-build-servers + ) + if [[ "$linked" == "true" ]]; then + restore_args+=(-p:AtomUILinkedPublish=true) + fi + dotnet "${restore_args[@]}" } run_fixture() { @@ -80,7 +89,12 @@ run_fixture() { --disable-build-servers ) if [[ "$generated" == "true" ]]; then - build_args+=(-p:AtomUIUseGeneratedRegistration=true) + # The generated-path snapshot is an explicit linked-publish verification. + # AtomUIUseGeneratedRegistration must remain inert for ordinary builds. + build_args+=(-p:AtomUILinkedPublish=true) + if [[ "$fixture" == "Minimal" ]]; then + build_args+=(-p:AtomUIEmitVerificationPlan=true) + fi fi dotnet "${build_args[@]}" @@ -109,8 +123,9 @@ json_number() { verify_non_trimmed_modes() { for fixture in "${fixtures[@]}"; do - restore_fixture "$fixture" + restore_fixture "$fixture" false run_fixture "$fixture" false "$output_root/$fixture.ordinary.json" + restore_fixture "$fixture" true run_fixture "$fixture" true "$output_root/$fixture.generated.json" done @@ -138,6 +153,7 @@ verify_non_trimmed_modes() { fi grep -q '"AtomUI:Button"' "$output_root/TwoUnits.generated.json" grep -q '"AtomUI:DatePicker"' "$output_root/TwoUnits.generated.json" + verify_minimal_plan cmp -s "$output_root/DynamicFallback.generated.json" "$output_root/DynamicFallback.ordinary.json" cmp -s "$output_root/Full.generated.json" "$output_root/Full.ordinary.json" @@ -149,6 +165,26 @@ verify_non_trimmed_modes() { done } +verify_minimal_plan() { + local plan="$fixture_root/Minimal/GeneratedFiles/AtomUI.Generator.LinkedPublish/AtomUI.Generator.LinkedRegistration.ApplicationRegistrationPlanGenerator/GeneratedApplicationRegistrationPlan.g.cs" + if [[ ! -f "$plan" ]]; then + printf 'Minimal generated application plan was not emitted: %s\n' "$plan" >&2 + return 1 + fi + + grep -q 'GeneratedRegistrationUnit_Button_' "$plan" + grep -q 'GeneratedRegistrationUnit_Space_' "$plan" + + local forbidden_unit + for forbidden_unit in DropdownButton SplitButton Flyouts Menu TreeView Dialog Tooltip DatePicker; do + if grep -q "GeneratedRegistrationUnit_${forbidden_unit}_" "$plan"; then + printf 'Minimal application plan unexpectedly retained the %s Unit.\n' \ + "$forbidden_unit" >&2 + return 1 + fi + done +} + publish_fixture() { local fixture="$1" local publish_kind="$2" @@ -186,7 +222,11 @@ publish_fixture() { } directory_size() { - find "$1" -type f -print0 | xargs -0 stat -f '%z' | awk '{ total += $1 } END { print total + 0 }' + find "$1" -type f \ + ! -path '*/.dSYM/*' \ + ! -name '*.pdb' \ + ! -name '*.dbg' \ + -print0 | xargs -0 stat -f '%z' | awk '{ total += $1 } END { print total + 0 }' } main_binary_size() { @@ -227,30 +267,38 @@ record_size() { } verify_size_gates() { - local minimal_size - local unused_unit_size - local full_size - minimal_size="$(awk -F '\t' '$5 == "aot" && $6 == "Minimal" { print $7 }' "$report_path")" - unused_unit_size="$(awk -F '\t' '$5 == "aot" && $6 == "MinimalWithUnusedUnit" { print $7 }' "$report_path")" - full_size="$(awk -F '\t' '$5 == "aot" && $6 == "Full" { print $7 }' "$report_path")" + local minimal_main_size + local unused_unit_main_size + local full_main_size + minimal_main_size="$(awk -F '\t' '$5 == "aot" && $6 == "Minimal" { print $8 }' "$report_path")" + unused_unit_main_size="$(awk -F '\t' '$5 == "aot" && $6 == "MinimalWithUnusedUnit" { print $8 }' "$report_path")" + full_main_size="$(awk -F '\t' '$5 == "aot" && $6 == "Full" { print $8 }' "$report_path")" - if [[ -z "$minimal_size" || -z "$unused_unit_size" || -z "$full_size" || "$full_size" -le 0 ]]; then + if [[ -z "$minimal_main_size" || -z "$unused_unit_main_size" || -z "$full_main_size" || + "$full_main_size" -le 0 ]]; then printf 'NativeAOT size report is incomplete: %s\n' "$report_path" >&2 return 1 fi - local reduction_percent=$(( (full_size - minimal_size) * 100 / full_size )) - local second_unit_growth=$(( unused_unit_size - minimal_size )) + local reduction_percent=$(( (full_main_size - minimal_main_size) * 100 / full_main_size )) + local second_unit_growth=$(( unused_unit_main_size - minimal_main_size )) if (( reduction_percent < MINIMUM_DESKTOP_REDUCTION_PERCENT )); then - printf 'NativeAOT Minimal reduction is %d%%; required minimum is %d%% (Minimal=%d, Full=%d).\n' \ - "$reduction_percent" "$MINIMUM_DESKTOP_REDUCTION_PERCENT" "$minimal_size" "$full_size" >&2 + printf 'NativeAOT Minimal main executable reduction is %d%%; required minimum is %d%% (Minimal=%d, Full=%d).\n' \ + "$reduction_percent" "$MINIMUM_DESKTOP_REDUCTION_PERCENT" \ + "$minimal_main_size" "$full_main_size" >&2 return 1 fi if (( second_unit_growth > MAX_SECOND_UNIT_GROWTH_BYTES )); then - printf 'Unused Unit NativeAOT growth is %d bytes; allowed maximum is %d bytes.\n' \ + printf 'Unused Unit NativeAOT main executable growth is %d bytes; allowed maximum is %d bytes.\n' \ "$second_unit_growth" "$MAX_SECOND_UNIT_GROWTH_BYTES" >&2 return 1 fi + if [[ "$rid" == "osx-arm64" ]] && + (( minimal_main_size > MAX_MINIMAL_MAIN_BYTES_OSX_ARM64 )); then + printf 'NativeAOT Minimal main executable is %d bytes; allowed osx-arm64 maximum is %d bytes.\n' \ + "$minimal_main_size" "$MAX_MINIMAL_MAIN_BYTES_OSX_ARM64" >&2 + return 1 + fi } verify_full_publish_matrix() { @@ -299,6 +347,7 @@ verify_full_publish_matrix() { verify_non_trimmed_modes if [[ "$mode" == "full" ]]; then verify_full_publish_matrix + printf 'Linked registration verification completed. Report: %s\n' "$report_path" +else + printf 'Linked registration quick verification completed. Snapshots: %s\n' "$output_root" fi - -printf 'Linked registration verification completed. Report: %s\n' "$report_path" diff --git a/src/AtomUI.Build.Tasks/AtomUI.Build.Tasks.csproj b/src/AtomUI.Build.Tasks/AtomUI.Build.Tasks.csproj index fe9ab405c..9470a5e9a 100644 --- a/src/AtomUI.Build.Tasks/AtomUI.Build.Tasks.csproj +++ b/src/AtomUI.Build.Tasks/AtomUI.Build.Tasks.csproj @@ -24,5 +24,11 @@ + + + diff --git a/src/AtomUI.Build.Tasks/GenerateLinkedRegistrationSidecarTask.cs b/src/AtomUI.Build.Tasks/GenerateLinkedRegistrationSidecarTask.cs new file mode 100644 index 000000000..7ad3fc2a2 --- /dev/null +++ b/src/AtomUI.Build.Tasks/GenerateLinkedRegistrationSidecarTask.cs @@ -0,0 +1,440 @@ +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using AtomUI.Generator.LinkedRegistration; +using AtomUI.Generator.LinkedRegistration.Manifest; +using AtomUI.LinkedRegistration.Protocol; +using Microsoft.Build.Framework; + +namespace AtomUI.Build.Tasks; + +public sealed class GenerateLinkedRegistrationSidecarTask : ITask +{ + private const string DiagnosticCode = "ATOMUILINK006"; + private const string AssemblyMetadataNamespace = "System.Reflection"; + private const string AssemblyMetadataName = "AssemblyMetadataAttribute"; + + [Required] + public string AssemblyPath { get; set; } = string.Empty; + + [Required] + public string OutputPath { get; set; } = string.Empty; + + public string TargetFramework { get; set; } = string.Empty; + + [Output] + public string SidecarPath { get; private set; } = string.Empty; + + [Output] + public bool WroteFile { get; private set; } + + public IBuildEngine BuildEngine { get; set; } = null!; + + public ITaskHost HostObject { get; set; } = null!; + + public bool Execute() + { + try + { + using var stream = File.OpenRead(AssemblyPath); + using var peReader = new PEReader(stream); + if (!peReader.HasMetadata) + { + return LogError("The file does not contain managed metadata."); + } + var reader = peReader.GetMetadataReader(); + if (!reader.IsAssembly) + { + return LogError("The file is not a managed assembly."); + } + + if (!TryReadRecords(reader, out var records, out var analysisBudgetExceeded, out var error)) + { + return LogError(error); + } + if (analysisBudgetExceeded) + { + records = records.Where(static record => + record is LinkedPackageManifestRecord || + record is LinkedUsageManifestRecord + { + Kind: LinkedUsageKind.Entry or LinkedUsageKind.PackageRoot + }) + .ToList(); + } + var assemblyName = reader.GetString(reader.GetAssemblyDefinition().Name); + if (!TryCreateSidecar(assemblyName, records, out var sidecar, out error)) + { + return LogError(error); + } + sidecar.Assembly.TargetFramework = TargetFramework.Trim(); + var bytes = LinkedRegistrationSidecarCodec.Write(sidecar); + if (analysisBudgetExceeded || + bytes.Length > LinkedRegistrationAnalysisBudget.MaxSidecarBytes) + { + sidecar = CreateBudgetFallbackSidecar(sidecar); + bytes = LinkedRegistrationSidecarCodec.Write(sidecar); + if (bytes.Length > LinkedRegistrationAnalysisBudget.MaxSidecarBytes) + { + return LogError( + $"the budget fallback Sidecar is {bytes.Length} bytes, exceeding the {LinkedRegistrationAnalysisBudget.MaxSidecarBytes} byte limit"); + } + } + var fullOutputPath = Path.GetFullPath(OutputPath); + Directory.CreateDirectory(Path.GetDirectoryName(fullOutputPath)!); + if (!File.Exists(fullOutputPath) || !File.ReadAllBytes(fullOutputPath).SequenceEqual(bytes)) + { + File.WriteAllBytes(fullOutputPath, bytes); + WroteFile = true; + } + SidecarPath = fullOutputPath; + return true; + } + catch (Exception exception) when ( + exception is IOException or UnauthorizedAccessException or ArgumentException or + BadImageFormatException or InvalidOperationException) + { + return LogError(exception.Message); + } + } + + private static bool TryReadRecords( + MetadataReader reader, + out List records, + out bool analysisBudgetExceeded, + out string error) + { + records = []; + analysisBudgetExceeded = false; + error = string.Empty; + foreach (var attributeHandle in reader.GetAssemblyDefinition().GetCustomAttributes()) + { + var attribute = reader.GetCustomAttribute(attributeHandle); + if (!IsAssemblyMetadataAttribute(reader, attribute.Constructor) || + !TryReadMetadataValue(reader, attribute, out var key, out var value) || + key is null || value is null || + !key.StartsWith(LinkedRegistrationProtocol.MetadataPrefix, StringComparison.Ordinal) || + key == LinkedRegistrationProtocol.PlanMarkerKey) + { + continue; + } + if (!LinkedRegistrationManifestCodec.TryDecode(key, value, out var record, out error)) + { + return false; + } + if (record is not null) + { + if (records.Count >= LinkedRegistrationAnalysisBudget.MaxManifestRecords && + record is not LinkedPackageManifestRecord && + record is not LinkedUsageManifestRecord + { + Kind: LinkedUsageKind.Entry or LinkedUsageKind.PackageRoot + }) + { + analysisBudgetExceeded = true; + } + else + { + records.Add(record); + } + } + } + return true; + } + + private static LinkedRegistrationSidecar CreateBudgetFallbackSidecar( + LinkedRegistrationSidecar sidecar) + { + var fallbackPackages = sidecar.Packages.Select(static package => package.Id) + .Distinct(StringComparer.Ordinal) + .OrderBy(static packageId => packageId, StringComparer.Ordinal) + .ToArray(); + var fallbacks = fallbackPackages.Length == 0 + ? new[] + { + new LinkedSidecarFallback + { + PackageId = string.Empty, + Reason = "AnalysisBudgetExceeded", + Source = "", + Line = 0, + Column = 0 + } + } + : fallbackPackages.Select(static packageId => new LinkedSidecarFallback + { + PackageId = packageId, + Reason = "AnalysisBudgetExceeded", + Source = "", + Line = 0, + Column = 0 + }).ToArray(); + var usages = sidecar.Usages + .Where(static usage => usage.Kind == nameof(LinkedUsageKind.Entry) || + usage.Kind == nameof(LinkedUsageKind.PackageRoot)) + .GroupBy(static usage => usage.Kind + "\u001f" + usage.Identity, StringComparer.Ordinal) + .Select(static group => group.First()) + .ToArray(); + return new LinkedRegistrationSidecar + { + ProtocolMajor = sidecar.ProtocolMajor, + ProtocolMinor = sidecar.ProtocolMinor, + Producer = sidecar.Producer, + Assembly = sidecar.Assembly, + Packages = sidecar.Packages.Select(static package => new LinkedSidecarPackage + { + Id = package.Id, + AssemblyName = package.AssemblyName, + Granularity = "Package", + EntryMethods = package.EntryMethods, + FullFragment = package.FullFragment, + SharedFragment = package.SharedFragment, + Units = [], + UnitEdges = [], + RootUnits = [] + }).ToArray(), + Usages = usages, + Fallbacks = fallbacks + }; + } + + private static bool TryCreateSidecar( + string assemblyName, + IReadOnlyList records, + out LinkedRegistrationSidecar sidecar, + out string error) + { + error = string.Empty; + var packageRecords = records.OfType() + .Distinct() + .OrderBy(static package => package.PackageId, StringComparer.Ordinal) + .ToArray(); + var unitRecords = records.OfType().Distinct().ToArray(); + var controlRecords = records.OfType().Distinct().ToArray(); + var edgeRecords = records.OfType().Distinct().ToArray(); + var rootRecords = records.OfType().Distinct().ToArray(); + + var packages = new List(packageRecords.Length); + foreach (var package in packageRecords) + { + if (!string.Equals(package.AssemblyName, assemblyName, StringComparison.Ordinal)) + { + sidecar = new LinkedRegistrationSidecar(); + error = $"Package '{package.PackageId}' declares assembly '{package.AssemblyName}', but the compiled assembly is '{assemblyName}'."; + return false; + } + var units = unitRecords.Where(unit => string.Equals( + unit.PackageId, + package.PackageId, + StringComparison.Ordinal)) + .OrderBy(static unit => unit.OrderKey) + .ThenBy(static unit => unit.UnitId, StringComparer.Ordinal) + .Select(unit => new LinkedSidecarUnit + { + Id = unit.UnitId, + FragmentType = unit.FragmentType, + FragmentMethod = unit.FragmentMethod, + OrderKey = unit.OrderKey, + Controls = controlRecords.Where(control => string.Equals( + control.PackageId, + package.PackageId, + StringComparison.Ordinal) && + string.Equals(control.UnitId, unit.UnitId, StringComparison.Ordinal)) + .Select(static control => control.MetadataName) + .Distinct(StringComparer.Ordinal) + .OrderBy(static control => control, StringComparer.Ordinal) + .ToArray() + }) + .ToArray(); + var unitIds = new HashSet(units.Select(static unit => unit.Id), StringComparer.Ordinal); + var packageEdges = edgeRecords.Where(edge => string.Equals( + edge.PackageId, + package.PackageId, + StringComparison.Ordinal)) + .ToArray(); + if (packageEdges.Any(edge => + !unitIds.Contains(edge.SourceUnitId) || !unitIds.Contains(edge.TargetUnitId))) + { + sidecar = new LinkedRegistrationSidecar(); + error = $"Package '{package.PackageId}' contains a UnitEdge that references an unknown Unit."; + return false; + } + var rootUnits = rootRecords.Where(root => string.Equals( + root.PackageId, + package.PackageId, + StringComparison.Ordinal)) + .Select(static root => root.UnitId) + .Distinct(StringComparer.Ordinal) + .OrderBy(static unitId => unitId, StringComparer.Ordinal) + .ToArray(); + if (rootUnits.Any(unitId => !unitIds.Contains(unitId))) + { + sidecar = new LinkedRegistrationSidecar(); + error = $"Package '{package.PackageId}' contains a Package Core root that references an unknown Unit."; + return false; + } + + packages.Add(new LinkedSidecarPackage + { + Id = package.PackageId, + AssemblyName = package.AssemblyName, + Granularity = package.Granularity.Length == 0 + ? (units.Length > 1 ? "Directory" : "Package") + : package.Granularity, + EntryMethods = SplitEntries(package.EntryMethodMetadataNames), + FullFragment = new LinkedSidecarFragment + { + Type = package.FullFragmentType, + Method = package.FullFragmentMethod + }, + SharedFragment = package.PackageSharedFragmentType is null + ? null + : new LinkedSidecarFragment + { + Type = package.PackageSharedFragmentType, + Method = package.PackageSharedFragmentMethod! + }, + Units = units, + UnitEdges = packageEdges.Select(static edge => new LinkedSidecarUnitEdge + { + SourceUnitId = edge.SourceUnitId, + TargetUnitId = edge.TargetUnitId, + EvidenceKind = edge.EvidenceKind.ToString() + }) + .ToArray(), + RootUnits = rootUnits + }); + } + + sidecar = new LinkedRegistrationSidecar + { + Producer = "AtomUI.Generator.LinkedPublish/6.0", + Assembly = new LinkedSidecarAssembly + { + Name = assemblyName + }, + Packages = packages.ToArray(), + Usages = records.OfType() + .Select(static usage => new LinkedSidecarUsage + { + Kind = usage.Kind.ToString(), + Identity = usage.Identity, + Source = NormalizeSource(usage.Source), + Line = usage.Line, + Column = usage.Column + }) + .ToArray(), + Fallbacks = records.OfType() + .Select(static fallback => new LinkedSidecarFallback + { + PackageId = fallback.PackageId, + Reason = fallback.Reason, + Source = NormalizeSource(fallback.Source), + Line = fallback.Line, + Column = fallback.Column + }) + .ToArray() + }; + return true; + } + + private static string[] SplitEntries(string entries) + { + return entries.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) + .Select(static entry => entry.Trim()) + .Where(static entry => entry.Length != 0) + .Distinct(StringComparer.Ordinal) + .OrderBy(static entry => entry, StringComparer.Ordinal) + .ToArray(); + } + + private static string NormalizeSource(string source) + { + var normalized = source.Replace('\\', '/'); + return Path.IsPathRooted(normalized) ? Path.GetFileName(normalized) : normalized; + } + + private static bool IsAssemblyMetadataAttribute(MetadataReader reader, EntityHandle constructor) + { + EntityHandle declaringType; + switch (constructor.Kind) + { + case HandleKind.MemberReference: + declaringType = reader.GetMemberReference((MemberReferenceHandle)constructor).Parent; + break; + case HandleKind.MethodDefinition: + declaringType = reader.GetMethodDefinition((MethodDefinitionHandle)constructor).GetDeclaringType(); + break; + default: + return false; + } + return IsType(reader, declaringType, AssemblyMetadataNamespace, AssemblyMetadataName); + } + + private static bool IsType( + MetadataReader reader, + EntityHandle handle, + string expectedNamespace, + string expectedName) + { + StringHandle namespaceHandle; + StringHandle nameHandle; + switch (handle.Kind) + { + case HandleKind.TypeReference: + var typeReference = reader.GetTypeReference((TypeReferenceHandle)handle); + namespaceHandle = typeReference.Namespace; + nameHandle = typeReference.Name; + break; + case HandleKind.TypeDefinition: + var typeDefinition = reader.GetTypeDefinition((TypeDefinitionHandle)handle); + namespaceHandle = typeDefinition.Namespace; + nameHandle = typeDefinition.Name; + break; + default: + return false; + } + return reader.StringComparer.Equals(namespaceHandle, expectedNamespace) && + reader.StringComparer.Equals(nameHandle, expectedName); + } + + private static bool TryReadMetadataValue( + MetadataReader reader, + CustomAttribute attribute, + out string? key, + out string? value) + { + key = null; + value = null; + try + { + var valueReader = reader.GetBlobReader(attribute.Value); + if (valueReader.ReadUInt16() != 1) + { + return false; + } + key = valueReader.ReadSerializedString(); + value = valueReader.ReadSerializedString(); + return key is not null && value is not null; + } + catch (BadImageFormatException) + { + return false; + } + } + + private bool LogError(string detail) + { + BuildEngine.LogErrorEvent(new BuildErrorEventArgs( + subcategory: "LinkedRegistration", + DiagnosticCode, + AssemblyPath, + lineNumber: 0, + columnNumber: 0, + endLineNumber: 0, + endColumnNumber: 0, + $"Cannot generate linked-registration Sidecar for '{AssemblyPath}': {detail}", + helpKeyword: null, + senderName: nameof(GenerateLinkedRegistrationSidecarTask))); + return false; + } +} diff --git a/src/AtomUI.Build.Tasks/LinkedRegistration/LinkedRegistrationAnalysisBudget.cs b/src/AtomUI.Build.Tasks/LinkedRegistration/LinkedRegistrationAnalysisBudget.cs new file mode 100644 index 000000000..d176c111e --- /dev/null +++ b/src/AtomUI.Build.Tasks/LinkedRegistration/LinkedRegistrationAnalysisBudget.cs @@ -0,0 +1,18 @@ +namespace AtomUI.LinkedRegistration.Protocol; + +internal static class LinkedRegistrationAnalysisBudget +{ + internal const int MaxSourceCandidates = 50_000; + internal const int MaxPackages = 1_024; + internal const int MaxRegistrationUnits = 2_048; + internal const int MaxUnitEdges = 10_000; + internal const int MaxRootUnits = 2_048; + internal const int MaxControlMappings = 50_000; + internal const int MaxUsages = 50_000; + internal const int MaxFallbacks = 4_096; + internal const int MaxManifestRecords = 64_000; + internal const int MaxSidecarBytes = 4 * 1024 * 1024; + internal const int MaxTotalSidecarBytes = 32 * 1024 * 1024; + internal const int MaxAxamlUsageBytes = 4 * 1024 * 1024; + internal const int MaxAxamlCandidates = 50_000; +} diff --git a/src/AtomUI.Build.Tasks/LinkedRegistration/LinkedRegistrationSidecar.cs b/src/AtomUI.Build.Tasks/LinkedRegistration/LinkedRegistrationSidecar.cs new file mode 100644 index 000000000..ceda438b7 --- /dev/null +++ b/src/AtomUI.Build.Tasks/LinkedRegistration/LinkedRegistrationSidecar.cs @@ -0,0 +1,350 @@ +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Security.Cryptography; +using System.Text; + +namespace AtomUI.LinkedRegistration.Protocol; + +[DataContract] +internal sealed class LinkedRegistrationSidecar +{ + internal const int SupportedProtocolMajor = 1; + internal const int SupportedProtocolMinor = 0; + + [DataMember(Name = "protocolMajor", Order = 0, IsRequired = true)] + public int ProtocolMajor { get; set; } = SupportedProtocolMajor; + + [DataMember(Name = "protocolMinor", Order = 1, IsRequired = true)] + public int ProtocolMinor { get; set; } = SupportedProtocolMinor; + + [DataMember(Name = "producer", Order = 2, IsRequired = true)] + public string Producer { get; set; } = string.Empty; + + [DataMember(Name = "assembly", Order = 3, IsRequired = true)] + public LinkedSidecarAssembly Assembly { get; set; } = new(); + + [DataMember(Name = "packages", Order = 4, IsRequired = true)] + public LinkedSidecarPackage[] Packages { get; set; } = []; + + [DataMember(Name = "usages", Order = 5, IsRequired = true)] + public LinkedSidecarUsage[] Usages { get; set; } = []; + + [DataMember(Name = "fallbacks", Order = 6, IsRequired = true)] + public LinkedSidecarFallback[] Fallbacks { get; set; } = []; +} + +[DataContract] +internal sealed class LinkedSidecarAssembly +{ + [DataMember(Name = "name", Order = 0, IsRequired = true)] + public string Name { get; set; } = string.Empty; + + [DataMember(Name = "contractHash", Order = 1, IsRequired = true)] + public string ContractHash { get; set; } = string.Empty; + + [DataMember(Name = "targetFramework", Order = 2, IsRequired = true)] + public string TargetFramework { get; set; } = string.Empty; +} + +[DataContract] +internal sealed class LinkedSidecarPackage +{ + [DataMember(Name = "id", Order = 0, IsRequired = true)] + public string Id { get; set; } = string.Empty; + + [DataMember(Name = "assemblyName", Order = 1, IsRequired = true)] + public string AssemblyName { get; set; } = string.Empty; + + [DataMember(Name = "granularity", Order = 2, IsRequired = true)] + public string Granularity { get; set; } = string.Empty; + + [DataMember(Name = "entryMethods", Order = 3, IsRequired = true)] + public string[] EntryMethods { get; set; } = []; + + [DataMember(Name = "fullFragment", Order = 4, IsRequired = true)] + public LinkedSidecarFragment FullFragment { get; set; } = new(); + + [DataMember(Name = "sharedFragment", Order = 5)] + public LinkedSidecarFragment? SharedFragment { get; set; } + + [DataMember(Name = "units", Order = 6, IsRequired = true)] + public LinkedSidecarUnit[] Units { get; set; } = []; + + [DataMember(Name = "unitEdges", Order = 7, IsRequired = true)] + public LinkedSidecarUnitEdge[] UnitEdges { get; set; } = []; + + [DataMember(Name = "rootUnits", Order = 8, IsRequired = true)] + public string[] RootUnits { get; set; } = []; +} + +[DataContract] +internal sealed class LinkedSidecarFragment +{ + [DataMember(Name = "type", Order = 0, IsRequired = true)] + public string Type { get; set; } = string.Empty; + + [DataMember(Name = "method", Order = 1, IsRequired = true)] + public string Method { get; set; } = string.Empty; +} + +[DataContract] +internal sealed class LinkedSidecarUnit +{ + [DataMember(Name = "id", Order = 0, IsRequired = true)] + public string Id { get; set; } = string.Empty; + + [DataMember(Name = "fragmentType", Order = 1, IsRequired = true)] + public string FragmentType { get; set; } = string.Empty; + + [DataMember(Name = "fragmentMethod", Order = 2, IsRequired = true)] + public string FragmentMethod { get; set; } = string.Empty; + + [DataMember(Name = "orderKey", Order = 3, IsRequired = true)] + public int OrderKey { get; set; } + + [DataMember(Name = "controls", Order = 4, IsRequired = true)] + public string[] Controls { get; set; } = []; +} + +[DataContract] +internal sealed class LinkedSidecarUnitEdge +{ + [DataMember(Name = "sourceUnitId", Order = 0, IsRequired = true)] + public string SourceUnitId { get; set; } = string.Empty; + + [DataMember(Name = "targetUnitId", Order = 1, IsRequired = true)] + public string TargetUnitId { get; set; } = string.Empty; + + [DataMember(Name = "evidenceKind", Order = 2, IsRequired = true)] + public string EvidenceKind { get; set; } = string.Empty; +} + +[DataContract] +internal sealed class LinkedSidecarUsage +{ + [DataMember(Name = "kind", Order = 0, IsRequired = true)] + public string Kind { get; set; } = string.Empty; + + [DataMember(Name = "identity", Order = 1, IsRequired = true)] + public string Identity { get; set; } = string.Empty; + + [DataMember(Name = "source", Order = 2, IsRequired = true)] + public string Source { get; set; } = string.Empty; + + [DataMember(Name = "line", Order = 3, IsRequired = true)] + public int Line { get; set; } + + [DataMember(Name = "column", Order = 4, IsRequired = true)] + public int Column { get; set; } +} + +[DataContract] +internal sealed class LinkedSidecarFallback +{ + [DataMember(Name = "packageId", Order = 0, IsRequired = true)] + public string PackageId { get; set; } = string.Empty; + + [DataMember(Name = "reason", Order = 1, IsRequired = true)] + public string Reason { get; set; } = string.Empty; + + [DataMember(Name = "source", Order = 2, IsRequired = true)] + public string Source { get; set; } = string.Empty; + + [DataMember(Name = "line", Order = 3, IsRequired = true)] + public int Line { get; set; } + + [DataMember(Name = "column", Order = 4, IsRequired = true)] + public int Column { get; set; } +} + +internal static class LinkedRegistrationSidecarCodec +{ + private static readonly DataContractJsonSerializer s_serializer = new( + typeof(LinkedRegistrationSidecar), + new DataContractJsonSerializerSettings + { + EmitTypeInformation = EmitTypeInformation.Never, + SerializeReadOnlyTypes = false, + UseSimpleDictionaryFormat = true + }); + + internal static byte[] Write(LinkedRegistrationSidecar sidecar) + { + Canonicalize(sidecar); + sidecar.Assembly.ContractHash = string.Empty; + sidecar.Assembly.ContractHash = ComputeHash(SerializeCore(sidecar)); + return SerializeCore(sidecar); + } + + internal static bool TryRead( + byte[] bytes, + out LinkedRegistrationSidecar? sidecar, + out string error) + { + sidecar = null; + error = string.Empty; + try + { + using var stream = new MemoryStream(bytes, writable: false); + sidecar = s_serializer.ReadObject(stream) as LinkedRegistrationSidecar; + if (sidecar is null) + { + error = "Sidecar root is missing."; + return false; + } + if (sidecar.ProtocolMajor != LinkedRegistrationSidecar.SupportedProtocolMajor) + { + error = $"Sidecar protocol major '{sidecar.ProtocolMajor}' is not supported."; + sidecar = null; + return false; + } + if (!ValidateRequiredFields(sidecar, out error)) + { + sidecar = null; + return false; + } + + var expectedHash = sidecar.Assembly.ContractHash; + Canonicalize(sidecar); + sidecar.Assembly.ContractHash = string.Empty; + var actualHash = ComputeHash(SerializeCore(sidecar)); + sidecar.Assembly.ContractHash = expectedHash; + if (!string.Equals(expectedHash, actualHash, StringComparison.Ordinal)) + { + error = $"Sidecar contract hash mismatch; expected '{expectedHash}', calculated '{actualHash}'."; + sidecar = null; + return false; + } + return true; + } + catch (Exception exception) when ( + exception is SerializationException or InvalidDataContractException or + IOException or ArgumentException) + { + error = exception.Message; + sidecar = null; + return false; + } + } + + private static void Canonicalize(LinkedRegistrationSidecar sidecar) + { + sidecar.Producer ??= string.Empty; + sidecar.Assembly ??= new LinkedSidecarAssembly(); + sidecar.Assembly.Name ??= string.Empty; + sidecar.Assembly.TargetFramework ??= string.Empty; + sidecar.Packages = (sidecar.Packages ?? []).OrderBy( + static package => package.Id, + StringComparer.Ordinal) + .ThenBy(static package => package.AssemblyName, StringComparer.Ordinal) + .ToArray(); + foreach (var package in sidecar.Packages) + { + package.EntryMethods = (package.EntryMethods ?? []).Where(static item => item.Length != 0) + .Distinct(StringComparer.Ordinal) + .OrderBy(static item => item, StringComparer.Ordinal) + .ToArray(); + package.Units = (package.Units ?? []).OrderBy(static unit => unit.OrderKey) + .ThenBy(static unit => unit.Id, StringComparer.Ordinal) + .ToArray(); + foreach (var unit in package.Units) + { + unit.Controls = (unit.Controls ?? []).Distinct(StringComparer.Ordinal) + .OrderBy(static control => control, StringComparer.Ordinal) + .ToArray(); + } + package.UnitEdges = (package.UnitEdges ?? []).GroupBy( + static edge => string.Join("\u001f", edge.SourceUnitId, edge.TargetUnitId, edge.EvidenceKind), + StringComparer.Ordinal) + .Select(static group => group.First()) + .OrderBy(static edge => edge.SourceUnitId, StringComparer.Ordinal) + .ThenBy(static edge => edge.TargetUnitId, StringComparer.Ordinal) + .ThenBy(static edge => edge.EvidenceKind, StringComparer.Ordinal) + .ToArray(); + package.RootUnits = (package.RootUnits ?? []).Distinct(StringComparer.Ordinal) + .OrderBy(static unitId => unitId, StringComparer.Ordinal) + .ToArray(); + } + sidecar.Usages = (sidecar.Usages ?? []).GroupBy( + static usage => string.Join( + "\u001f", + usage.Kind, + usage.Identity, + usage.Source, + usage.Line.ToString(System.Globalization.CultureInfo.InvariantCulture), + usage.Column.ToString(System.Globalization.CultureInfo.InvariantCulture)), + StringComparer.Ordinal) + .Select(static group => group.First()) + .OrderBy(static usage => usage.Source, StringComparer.Ordinal) + .ThenBy(static usage => usage.Line) + .ThenBy(static usage => usage.Column) + .ThenBy(static usage => usage.Kind, StringComparer.Ordinal) + .ThenBy(static usage => usage.Identity, StringComparer.Ordinal) + .ToArray(); + sidecar.Fallbacks = (sidecar.Fallbacks ?? []).GroupBy( + static fallback => string.Join( + "\u001f", + fallback.PackageId, + fallback.Reason, + fallback.Source, + fallback.Line.ToString(System.Globalization.CultureInfo.InvariantCulture), + fallback.Column.ToString(System.Globalization.CultureInfo.InvariantCulture)), + StringComparer.Ordinal) + .Select(static group => group.First()) + .OrderBy(static fallback => fallback.PackageId, StringComparer.Ordinal) + .ThenBy(static fallback => fallback.Source, StringComparer.Ordinal) + .ThenBy(static fallback => fallback.Line) + .ThenBy(static fallback => fallback.Column) + .ThenBy(static fallback => fallback.Reason, StringComparer.Ordinal) + .ToArray(); + } + + private static bool ValidateRequiredFields( + LinkedRegistrationSidecar sidecar, + out string error) + { + error = string.Empty; + if (sidecar.Assembly is null || string.IsNullOrWhiteSpace(sidecar.Assembly.Name) || + string.IsNullOrWhiteSpace(sidecar.Assembly.ContractHash) || + sidecar.Packages is null || sidecar.Usages is null || sidecar.Fallbacks is null) + { + error = "Sidecar is missing required top-level fields."; + return false; + } + foreach (var package in sidecar.Packages) + { + if (string.IsNullOrWhiteSpace(package.Id) || + string.IsNullOrWhiteSpace(package.AssemblyName) || + (package.Granularity != "Package" && package.Granularity != "Directory") || + package.FullFragment is null || + string.IsNullOrWhiteSpace(package.FullFragment.Type) || + string.IsNullOrWhiteSpace(package.FullFragment.Method) || + package.EntryMethods is null || package.Units is null || + package.UnitEdges is null || package.RootUnits is null) + { + error = $"Package '{package.Id}' contains invalid required fields."; + return false; + } + } + return true; + } + + private static byte[] SerializeCore(LinkedRegistrationSidecar sidecar) + { + using var stream = new MemoryStream(); + s_serializer.WriteObject(stream, sidecar); + return stream.ToArray(); + } + + private static string ComputeHash(byte[] bytes) + { + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(bytes); + var builder = new StringBuilder("sha256:", 7 + hash.Length * 2); + foreach (var value in hash) + { + builder.Append(value.ToString("x2", System.Globalization.CultureInfo.InvariantCulture)); + } + return builder.ToString(); + } +} diff --git a/src/AtomUI.Core/Registration/AotTrimControlPackageRegistrationBuilder.cs b/src/AtomUI.Core/Registration/AotTrimControlPackageRegistrationBuilder.cs index 0cef4085a..4a7555fad 100644 --- a/src/AtomUI.Core/Registration/AotTrimControlPackageRegistrationBuilder.cs +++ b/src/AtomUI.Core/Registration/AotTrimControlPackageRegistrationBuilder.cs @@ -13,7 +13,7 @@ public sealed class AotTrimControlPackageRegistrationBuilder private readonly Func? _includeIdentity; private readonly Func, IReadOnlyList>? _selectAssets; - private readonly HashSet _enteredUnits = new(StringComparer.Ordinal); + private HashSet? _enteredUnits; private readonly List _controls = new(); private readonly List _themeAssets = new(); private readonly List> _packageSharedThemeAssets = new(); @@ -40,7 +40,7 @@ public sealed class AotTrimControlPackageRegistrationBuilder { ThrowIfRegistered(); ArgumentException.ThrowIfNullOrWhiteSpace(unitId); - return _enteredUnits.Add(unitId); + return (_enteredUnits ??= new HashSet(StringComparer.Ordinal)).Add(unitId); } public void AddControl(ControlTokenDescriptor descriptor) diff --git a/src/AtomUI.Desktop.Controls/Buttons/Button.cs b/src/AtomUI.Desktop.Controls/Button/Button.cs similarity index 100% rename from src/AtomUI.Desktop.Controls/Buttons/Button.cs rename to src/AtomUI.Desktop.Controls/Button/Button.cs diff --git a/src/AtomUI.Desktop.Controls/Buttons/ButtonToken.cs b/src/AtomUI.Desktop.Controls/Button/ButtonToken.cs similarity index 100% rename from src/AtomUI.Desktop.Controls/Buttons/ButtonToken.cs rename to src/AtomUI.Desktop.Controls/Button/ButtonToken.cs diff --git a/src/AtomUI.Desktop.Controls/Buttons/HyperLinkButton.cs b/src/AtomUI.Desktop.Controls/Button/HyperLinkButton.cs similarity index 100% rename from src/AtomUI.Desktop.Controls/Buttons/HyperLinkButton.cs rename to src/AtomUI.Desktop.Controls/Button/HyperLinkButton.cs diff --git a/src/AtomUI.Desktop.Controls/Buttons/HyperLinkButtonToken.cs b/src/AtomUI.Desktop.Controls/Button/HyperLinkButtonToken.cs similarity index 100% rename from src/AtomUI.Desktop.Controls/Buttons/HyperLinkButtonToken.cs rename to src/AtomUI.Desktop.Controls/Button/HyperLinkButtonToken.cs diff --git a/src/AtomUI.Desktop.Controls/Buttons/IconButton.cs b/src/AtomUI.Desktop.Controls/Button/IconButton.cs similarity index 100% rename from src/AtomUI.Desktop.Controls/Buttons/IconButton.cs rename to src/AtomUI.Desktop.Controls/Button/IconButton.cs diff --git a/src/AtomUI.Desktop.Controls/Buttons/ToggleIconButton.cs b/src/AtomUI.Desktop.Controls/Button/ToggleIconButton.cs similarity index 100% rename from src/AtomUI.Desktop.Controls/Buttons/ToggleIconButton.cs rename to src/AtomUI.Desktop.Controls/Button/ToggleIconButton.cs diff --git a/src/AtomUI.Desktop.Controls/Buttons/DropdownButton.cs b/src/AtomUI.Desktop.Controls/DropdownButton/DropdownButton.cs similarity index 100% rename from src/AtomUI.Desktop.Controls/Buttons/DropdownButton.cs rename to src/AtomUI.Desktop.Controls/DropdownButton/DropdownButton.cs diff --git a/src/AtomUI.Desktop.Controls/Buttons/DropdownButtonToken.cs b/src/AtomUI.Desktop.Controls/DropdownButton/DropdownButtonToken.cs similarity index 100% rename from src/AtomUI.Desktop.Controls/Buttons/DropdownButtonToken.cs rename to src/AtomUI.Desktop.Controls/DropdownButton/DropdownButtonToken.cs diff --git a/src/AtomUI.Desktop.Controls/Dialog/DialogInputCaptureTracker.cs b/src/AtomUI.Desktop.Controls/PackageCore/DialogInputCaptureTracker.cs similarity index 100% rename from src/AtomUI.Desktop.Controls/Dialog/DialogInputCaptureTracker.cs rename to src/AtomUI.Desktop.Controls/PackageCore/DialogInputCaptureTracker.cs diff --git a/src/AtomUI.Desktop.Controls/Tooltip/ToolTipService.cs b/src/AtomUI.Desktop.Controls/PackageCore/ToolTipService.cs similarity index 100% rename from src/AtomUI.Desktop.Controls/Tooltip/ToolTipService.cs rename to src/AtomUI.Desktop.Controls/PackageCore/ToolTipService.cs diff --git a/src/AtomUI.Desktop.Controls/Buttons/SplitButton.cs b/src/AtomUI.Desktop.Controls/SplitButton/SplitButton.cs similarity index 100% rename from src/AtomUI.Desktop.Controls/Buttons/SplitButton.cs rename to src/AtomUI.Desktop.Controls/SplitButton/SplitButton.cs diff --git a/src/AtomUI.Desktop.Controls/Buttons/SplitButtonToken.cs b/src/AtomUI.Desktop.Controls/SplitButton/SplitButtonToken.cs similarity index 100% rename from src/AtomUI.Desktop.Controls/Buttons/SplitButtonToken.cs rename to src/AtomUI.Desktop.Controls/SplitButton/SplitButtonToken.cs diff --git a/src/AtomUI.Generator.LinkedPublish/AtomUI.Generator.LinkedPublish.csproj b/src/AtomUI.Generator.LinkedPublish/AtomUI.Generator.LinkedPublish.csproj new file mode 100644 index 000000000..942b7b802 --- /dev/null +++ b/src/AtomUI.Generator.LinkedPublish/AtomUI.Generator.LinkedPublish.csproj @@ -0,0 +1,48 @@ + + + netstandard2.0 + AtomUI.Generator.LinkedPublish + AtomUI.Generator + true + true + true + false + false + false + false + false + false + false + false + false + false + false + false + + + + + + + + + + + + + + + + + + + + diff --git a/src/AtomUI.Generator.LinkedPublish/LinkedCSharpUsageCandidateProvider.cs b/src/AtomUI.Generator.LinkedPublish/LinkedCSharpUsageCandidateProvider.cs new file mode 100644 index 000000000..2fbadf47d --- /dev/null +++ b/src/AtomUI.Generator.LinkedPublish/LinkedCSharpUsageCandidateProvider.cs @@ -0,0 +1,352 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Text; + +namespace AtomUI.Generator.LinkedRegistration; + +internal static class LinkedCSharpUsageCandidateProvider +{ + internal static IncrementalValuesProvider Create( + IncrementalGeneratorInitializationContext context) + { + return context.SyntaxProvider.CreateSyntaxProvider( + static (node, _) => IsCandidate(node), + static (syntaxContext, cancellationToken) => Transform( + syntaxContext, + cancellationToken)) + .SelectMany(static (candidates, _) => candidates); + } + + private static bool IsCandidate(SyntaxNode node) + { + return node is ObjectCreationExpressionSyntax or + ImplicitObjectCreationExpressionSyntax or + TypeOfExpressionSyntax or + InvocationExpressionSyntax or + TypeDeclarationSyntax; + } + + private static ImmutableArray Transform( + GeneratorSyntaxContext context, + CancellationToken cancellationToken) + { + var builder = ImmutableArray.CreateBuilder(); + switch (context.Node) + { + case ObjectCreationExpressionSyntax objectCreation + when context.SemanticModel.GetOperation(objectCreation, cancellationToken) is + IObjectCreationOperation operation: + AddType(builder, operation.Type, objectCreation.GetLocation()); + AddCall(builder, operation.Constructor, objectCreation.GetLocation()); + break; + case ImplicitObjectCreationExpressionSyntax implicitCreation + when context.SemanticModel.GetOperation(implicitCreation, cancellationToken) is + IObjectCreationOperation operation: + AddType(builder, operation.Type, implicitCreation.GetLocation()); + AddCall(builder, operation.Constructor, implicitCreation.GetLocation()); + break; + case TypeOfExpressionSyntax typeOfExpression + when context.SemanticModel.GetOperation(typeOfExpression, cancellationToken) is + ITypeOfOperation operation: + AddType(builder, operation.TypeOperand, typeOfExpression.GetLocation()); + break; + case InvocationExpressionSyntax invocation + when context.SemanticModel.GetOperation(invocation, cancellationToken) is + IInvocationOperation operation: + CollectInvocation( + builder, + operation, + context.SemanticModel, + cancellationToken); + break; + case InvocationExpressionSyntax invocation + when context.SemanticModel.GetOperation(invocation, cancellationToken) is + IDynamicInvocationOperation: + builder.Add(LinkedCSharpUsageCandidate.Create( + LinkedCSharpUsageCandidateKind.Dynamic, + "dynamic invocation", + invocation.GetLocation())); + break; + case TypeDeclarationSyntax declaration + when context.SemanticModel.GetDeclaredSymbol(declaration, cancellationToken) is + INamedTypeSymbol type: + AddType(builder, type.BaseType, declaration.Identifier.GetLocation()); + break; + } + return builder.ToImmutable(); + } + + private static void CollectInvocation( + ImmutableArray.Builder builder, + IInvocationOperation invocation, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + var target = invocation.TargetMethod; + AddCall(builder, target, invocation.Syntax.GetLocation()); + AddControlResultType(builder, target, invocation.Syntax.GetLocation()); + target = target.OriginalDefinition; + if (!IsDynamicCreationMethod(target)) + { + return; + } + + if (invocation.TargetMethod.IsGenericMethod && + invocation.TargetMethod.TypeArguments.Length != 0 && + invocation.TargetMethod.TypeArguments.All(static type => !ContainsTypeParameter(type))) + { + foreach (var typeArgument in invocation.TargetMethod.TypeArguments) + { + AddType(builder, typeArgument, invocation.Syntax.GetLocation()); + } + return; + } + + var resolvedType = ResolveRuntimeTypeValue( + invocation.Arguments.FirstOrDefault()?.Value, + semanticModel, + new HashSet(SymbolEqualityComparer.Default), + depth: 0, + cancellationToken); + if (resolvedType is not null && !ContainsTypeParameter(resolvedType)) + { + AddType(builder, resolvedType, invocation.Syntax.GetLocation()); + return; + } + + builder.Add(LinkedCSharpUsageCandidate.Create( + LinkedCSharpUsageCandidateKind.Dynamic, + LinkedRegistrationSymbolName.GetMethodMetadataName(target), + invocation.Syntax.GetLocation())); + } + + private static void AddControlResultType( + ImmutableArray.Builder builder, + IMethodSymbol method, + Location location) + { + var type = method.OriginalDefinition.ReturnType; + if (type is INamedTypeSymbol named && IsControlType(named)) + { + builder.Add(LinkedCSharpUsageCandidate.Create( + LinkedCSharpUsageCandidateKind.Type, + LinkedRegistrationSymbolName.GetTypeMetadataName(named.OriginalDefinition), + location)); + } + } + + private static ITypeSymbol? ResolveRuntimeTypeValue( + IOperation? operation, + SemanticModel semanticModel, + ISet visitedSymbols, + int depth, + CancellationToken cancellationToken) + { + if (depth >= 8) + { + return null; + } + while (operation is IConversionOperation conversion) + { + operation = conversion.Operand; + } + if (operation is ITypeOfOperation typeOfOperation) + { + return typeOfOperation.TypeOperand; + } + if (operation is not ILocalReferenceOperation localReference || + !visitedSymbols.Add(localReference.Local)) + { + return null; + } + + foreach (var syntaxReference in localReference.Local.DeclaringSyntaxReferences) + { + cancellationToken.ThrowIfCancellationRequested(); + var syntax = syntaxReference.GetSyntax(cancellationToken); + if (!ReferenceEquals(syntax.SyntaxTree, semanticModel.SyntaxTree) || + semanticModel.GetOperation(syntax, cancellationToken) is not + IVariableDeclaratorOperation declarator || + declarator.Initializer is null) + { + continue; + } + var resolved = ResolveRuntimeTypeValue( + declarator.Initializer.Value, + semanticModel, + visitedSymbols, + depth + 1, + cancellationToken); + if (resolved is not null) + { + return resolved; + } + } + return null; + } + + private static void AddType( + ImmutableArray.Builder builder, + ITypeSymbol? type, + Location location) + { + switch (type) + { + case IArrayTypeSymbol array: + AddType(builder, array.ElementType, location); + break; + case IPointerTypeSymbol pointer: + AddType(builder, pointer.PointedAtType, location); + break; + case INamedTypeSymbol named: + builder.Add(LinkedCSharpUsageCandidate.Create( + LinkedCSharpUsageCandidateKind.Type, + LinkedRegistrationSymbolName.GetTypeMetadataName(named.OriginalDefinition), + location)); + foreach (var typeArgument in named.TypeArguments) + { + AddType(builder, typeArgument, location); + } + break; + } + } + + private static void AddCall( + ImmutableArray.Builder builder, + IMethodSymbol? method, + Location location) + { + if (method is null) + { + return; + } + builder.Add(LinkedCSharpUsageCandidate.Create( + LinkedCSharpUsageCandidateKind.Call, + LinkedRegistrationSymbolName.GetMethodMetadataName(method.OriginalDefinition), + location, + LinkedRegistrationSymbolName.GetTypeMetadataName( + method.ContainingType.OriginalDefinition), + method.ContainingType.TypeKind == TypeKind.Interface, + method.ContainingType.TypeKind == TypeKind.Delegate || + method.MethodKind == MethodKind.DelegateInvoke)); + } + + private static bool IsDynamicCreationMethod(IMethodSymbol method) + { + var containingType = LinkedRegistrationSymbolName.GetTypeMetadataName( + method.ContainingType.OriginalDefinition); + return (containingType == "System.Activator" && method.Name == "CreateInstance") || + (containingType == "System.Runtime.CompilerServices.RuntimeHelpers" && + method.Name == "GetUninitializedObject") || + (containingType == "System.Runtime.Serialization.FormatterServices" && + method.Name == "GetUninitializedObject"); + } + + private static bool IsControlType(INamedTypeSymbol type) + { + for (var current = type; current is not null; current = current.BaseType) + { + if (LinkedRegistrationSymbolName.GetTypeMetadataName(current.OriginalDefinition) == + "Avalonia.Controls.Control") + { + return true; + } + } + return false; + } + + private static bool ContainsTypeParameter(ITypeSymbol type) + { + return type switch + { + ITypeParameterSymbol => true, + IArrayTypeSymbol array => ContainsTypeParameter(array.ElementType), + IPointerTypeSymbol pointer => ContainsTypeParameter(pointer.PointedAtType), + INamedTypeSymbol named => named.TypeArguments.Any(ContainsTypeParameter) || + (named.ContainingType is not null && + ContainsTypeParameter(named.ContainingType)), + _ => false + }; + } +} + +internal enum LinkedCSharpUsageCandidateKind +{ + Type, + Call, + Dynamic +} + +internal sealed class LinkedCSharpUsageCandidate +{ + private LinkedCSharpUsageCandidate( + LinkedCSharpUsageCandidateKind kind, + string identity, + string containingTypeIdentity, + string source, + int spanStart, + int spanLength, + int line, + int column, + bool isInterfaceDispatch, + bool isOpenDispatch) + { + Kind = kind; + Identity = identity; + ContainingTypeIdentity = containingTypeIdentity; + Source = source; + SpanStart = spanStart; + SpanLength = spanLength; + Line = line; + Column = column; + IsInterfaceDispatch = isInterfaceDispatch; + IsOpenDispatch = isOpenDispatch; + } + + internal LinkedCSharpUsageCandidateKind Kind { get; } + internal string Identity { get; } + internal string ContainingTypeIdentity { get; } + internal string Source { get; } + internal int SpanStart { get; } + internal int SpanLength { get; } + internal int Line { get; } + internal int Column { get; } + internal bool IsInterfaceDispatch { get; } + internal bool IsOpenDispatch { get; } + + internal static LinkedCSharpUsageCandidate Create( + LinkedCSharpUsageCandidateKind kind, + string identity, + Location location, + string containingTypeIdentity = "", + bool isInterfaceDispatch = false, + bool isOpenDispatch = false) + { + var lineSpan = location.GetLineSpan(); + var source = lineSpan.Path.Length != 0 + ? lineSpan.Path + : location.SourceTree?.FilePath ?? string.Empty; + return new LinkedCSharpUsageCandidate( + kind, + identity, + containingTypeIdentity, + source, + location.SourceSpan.Start, + location.SourceSpan.Length, + lineSpan.StartLinePosition.Line + 1, + lineSpan.StartLinePosition.Character + 1, + isInterfaceDispatch, + isOpenDispatch); + } + + internal Location CreateLocation() + { + var start = new LinePosition(Math.Max(0, Line - 1), Math.Max(0, Column - 1)); + return Location.Create( + Source.Length == 0 ? "" : Source, + new Microsoft.CodeAnalysis.Text.TextSpan(SpanStart, SpanLength), + new LinePositionSpan(start, start)); + } +} diff --git a/src/AtomUI.Generator.LinkedPublish/LinkedRegistrationPackageManifestGenerator.cs b/src/AtomUI.Generator.LinkedPublish/LinkedRegistrationPackageManifestGenerator.cs new file mode 100644 index 000000000..6b4aedd92 --- /dev/null +++ b/src/AtomUI.Generator.LinkedPublish/LinkedRegistrationPackageManifestGenerator.cs @@ -0,0 +1,497 @@ +using System.Collections.Immutable; +using System.Text; +using AtomUI.Generator.Diagnostics; +using AtomUI.Generator.LinkedRegistration.Manifest; +using AtomUI.Generator.LinkedRegistration.Model; +using AtomUI.LinkedRegistration.Protocol; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace AtomUI.Generator.LinkedRegistration; + +[Generator] +public sealed class LinkedRegistrationPackageManifestGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var candidates = LinkedCSharpUsageCandidateProvider.Create(context).Collect(); + var input = context.CompilationProvider.Combine(candidates) + .Combine(context.AnalyzerConfigOptionsProvider); + context.RegisterSourceOutput(input, static (productionContext, value) => + Generate( + productionContext, + value.Left.Left, + value.Left.Right, + value.Right)); + } + + private static void Generate( + SourceProductionContext context, + Compilation compilation, + ImmutableArray candidates, + AnalyzerConfigOptionsProvider optionsProvider) + { + var assemblyName = compilation.AssemblyName ?? "AtomUI"; + var packageId = LinkedRegistrationOptions.GetDeclaredPackageId(optionsProvider); + if (packageId.Length == 0 || + LinkedRegistrationOptions.GetRegistrationGranularity(optionsProvider, out _) != + RegistrationUnitGranularity.Directory) + { + context.AddSource( + "LinkedRegistrationPackageAnalysis.g.cs", + GeneratedSourceText.From("// \n#nullable enable\n")); + return; + } + + var projectDirectory = LinkedRegistrationOptions.GetProjectDirectory(optionsProvider); + var typeUnits = BuildTypeUnitMap( + compilation, + packageId, + projectDirectory, + optionsProvider, + out var knownUnits, + out var controlTypes); + var edges = new HashSet(UnitEdgeComparer.Instance); + var rootUnits = new HashSet(StringComparer.Ordinal); + var fallbacks = new HashSet(FallbackComparer.Instance); + + if (candidates.Length > LinkedRegistrationAnalysisBudget.MaxSourceCandidates || + knownUnits.Count > LinkedRegistrationAnalysisBudget.MaxRegistrationUnits) + { + fallbacks.Add(new Fallback( + packageId, + "AnalysisBudgetExceeded", + assemblyName, + 0, + 0, + Location.None)); + } + else + { + var treesByPath = compilation.SyntaxTrees + .Where(static tree => tree.FilePath.Length != 0) + .GroupBy(static tree => tree.FilePath, StringComparer.Ordinal) + .ToDictionary( + static group => group.Key, + static group => group.First(), + StringComparer.Ordinal); + foreach (var candidate in candidates.OrderBy( + static item => item.Source, + StringComparer.Ordinal).ThenBy(static item => item.SpanStart) + .ThenBy(static item => item.Kind) + .ThenBy(static item => item.Identity, StringComparer.Ordinal)) + { + context.CancellationToken.ThrowIfCancellationRequested(); + if (candidate.Kind == LinkedCSharpUsageCandidateKind.Dynamic) + { + fallbacks.Add(new Fallback( + packageId, + "DynamicInvocation", + NormalizeSourcePath(candidate.Source, projectDirectory), + candidate.Line, + candidate.Column, + candidate.CreateLocation())); + continue; + } + + var targetType = candidate.Kind == LinkedCSharpUsageCandidateKind.Type + ? candidate.Identity + : candidate.ContainingTypeIdentity; + if (!typeUnits.TryGetValue(targetType, out var targetUnitId)) + { + if (controlTypes.Contains(targetType)) + { + fallbacks.Add(new Fallback( + packageId, + "UnresolvedOwner", + NormalizeSourcePath(candidate.Source, projectDirectory), + candidate.Line, + candidate.Column, + candidate.CreateLocation())); + } + continue; + } + + var sourceUnitId = GetUnitIdForSource( + candidate.Source, + packageId, + projectDirectory, + optionsProvider, + treesByPath, + knownUnits); + if (sourceUnitId is null) + { + if (candidate.Kind == LinkedCSharpUsageCandidateKind.Type) + { + rootUnits.Add(targetUnitId); + } + continue; + } + if (string.Equals(sourceUnitId, targetUnitId, StringComparison.Ordinal)) + { + continue; + } + if (candidate.IsInterfaceDispatch) + { + continue; + } + if (candidate.IsOpenDispatch) + { + fallbacks.Add(new Fallback( + packageId, + "UnresolvedOwner", + NormalizeSourcePath(candidate.Source, projectDirectory), + candidate.Line, + candidate.Column, + candidate.CreateLocation())); + continue; + } + + edges.Add(new UnitEdge( + packageId, + sourceUnitId, + targetUnitId, + candidate.Kind == LinkedCSharpUsageCandidateKind.Call + ? LinkedUnitEdgeEvidenceKind.CSharpCall + : LinkedUnitEdgeEvidenceKind.CSharpType)); + if (edges.Count > LinkedRegistrationAnalysisBudget.MaxUnitEdges) + { + edges.Clear(); + rootUnits.Clear(); + fallbacks.Add(new Fallback( + packageId, + "AnalysisBudgetExceeded", + assemblyName, + 0, + 0, + Location.None)); + break; + } + } + } + + var source = new StringBuilder(); + source.AppendLine("// "); + source.AppendLine("#nullable enable"); + foreach (var edge in edges.OrderBy(static edge => edge.SourceUnitId, StringComparer.Ordinal) + .ThenBy(static edge => edge.TargetUnitId, StringComparer.Ordinal) + .ThenBy(static edge => edge.EvidenceKind)) + { + LinkedRegistrationMetadataWriter.Write( + source, + new LinkedUnitEdgeManifestRecord( + edge.PackageId, + edge.SourceUnitId, + edge.TargetUnitId, + edge.EvidenceKind)); + } + foreach (var unitId in rootUnits.OrderBy(static unitId => unitId, StringComparer.Ordinal)) + { + LinkedRegistrationMetadataWriter.Write( + source, + new LinkedRootUnitManifestRecord(packageId, unitId)); + } + foreach (var fallback in fallbacks.OrderBy(static item => item.Source, StringComparer.Ordinal) + .ThenBy(static item => item.Line) + .ThenBy(static item => item.Column) + .ThenBy(static item => item.Reason, StringComparer.Ordinal)) + { + LinkedRegistrationMetadataWriter.Write( + source, + new LinkedFallbackManifestRecord( + fallback.PackageId, + fallback.Reason, + fallback.Source, + fallback.Line, + fallback.Column)); + ReportFallback(context, optionsProvider, fallback); + } + context.AddSource( + "LinkedRegistrationPackageAnalysis.g.cs", + GeneratedSourceText.From(source.ToString())); + } + + private static Dictionary BuildTypeUnitMap( + Compilation compilation, + string packageId, + string projectDirectory, + AnalyzerConfigOptionsProvider optionsProvider, + out HashSet knownUnits, + out HashSet controlTypes) + { + var types = GetTypes(compilation.Assembly.GlobalNamespace).ToArray(); + var candidateUnits = new Dictionary(SymbolEqualityComparer.Default); + knownUnits = new HashSet(StringComparer.Ordinal); + controlTypes = new HashSet(StringComparer.Ordinal); + foreach (var type in types) + { + var unitId = GetUnitIdForType( + type, + packageId, + projectDirectory, + optionsProvider); + candidateUnits[type] = unitId; + if (IsControlType(type)) + { + controlTypes.Add(LinkedRegistrationSymbolName.GetTypeMetadataName(type)); + if (unitId is not null) + { + knownUnits.Add(unitId); + } + } + } + + var result = new Dictionary(StringComparer.Ordinal); + foreach (var pair in candidateUnits) + { + if (pair.Value is not null && knownUnits.Contains(pair.Value)) + { + result[LinkedRegistrationSymbolName.GetTypeMetadataName(pair.Key)] = pair.Value; + } + } + return result; + } + + private static string? GetUnitIdForType( + INamedTypeSymbol type, + string packageId, + string projectDirectory, + AnalyzerConfigOptionsProvider optionsProvider) + { + var candidates = type.DeclaringSyntaxReferences.Select(reference => + CreateUnitId( + reference.SyntaxTree, + packageId, + projectDirectory, + type.Name, + optionsProvider)) + .Distinct(StringComparer.Ordinal) + .ToArray(); + return candidates.Length == 1 ? candidates[0] : null; + } + + private static string? GetUnitIdForSource( + string source, + string packageId, + string projectDirectory, + AnalyzerConfigOptionsProvider optionsProvider, + IReadOnlyDictionary treesByPath, + ISet knownUnits) + { + if (!treesByPath.TryGetValue(source, out var tree)) + { + return null; + } + var unitId = CreateUnitId( + tree, + packageId, + projectDirectory, + Path.GetFileNameWithoutExtension(source), + optionsProvider); + return knownUnits.Contains(unitId) ? unitId : null; + } + + private static string CreateUnitId( + SyntaxTree tree, + string packageId, + string projectDirectory, + string fallbackName, + AnalyzerConfigOptionsProvider optionsProvider) + { + optionsProvider.GetOptions(tree).TryGetValue( + "build_metadata.Compile.AtomUIRegistrationUnit", + out var explicitUnit); + return RegistrationUnitId.Create( + packageId, + RegistrationUnitGranularity.Directory, + tree.FilePath, + projectDirectory, + fallbackName, + explicitUnit); + } + + private static IEnumerable GetTypes(INamespaceSymbol ns) + { + foreach (var type in ns.GetTypeMembers().OrderBy( + static type => type.MetadataName, + StringComparer.Ordinal)) + { + yield return type; + foreach (var nested in GetNestedTypes(type)) + { + yield return nested; + } + } + foreach (var child in ns.GetNamespaceMembers().OrderBy( + static child => child.Name, + StringComparer.Ordinal)) + { + foreach (var type in GetTypes(child)) + { + yield return type; + } + } + } + + private static IEnumerable GetNestedTypes(INamedTypeSymbol type) + { + foreach (var nested in type.GetTypeMembers().OrderBy( + static item => item.MetadataName, + StringComparer.Ordinal)) + { + yield return nested; + foreach (var descendant in GetNestedTypes(nested)) + { + yield return descendant; + } + } + } + + private static bool IsControlType(INamedTypeSymbol type) + { + for (var current = type; current is not null; current = current.BaseType) + { + if (LinkedRegistrationSymbolName.GetTypeMetadataName(current.OriginalDefinition) == + "Avalonia.Controls.Control") + { + return true; + } + } + return false; + } + + private static string NormalizeSourcePath(string path, string projectDirectory) + { + var normalized = path.Replace('\\', '/'); + var root = projectDirectory.Replace('\\', '/').TrimEnd('/'); + if (root.Length != 0 && normalized.StartsWith(root + "/", StringComparison.Ordinal)) + { + return normalized.Substring(root.Length + 1); + } + return normalized.Length == 0 ? "" : normalized; + } + + private static void ReportFallback( + SourceProductionContext context, + AnalyzerConfigOptionsProvider optionsProvider, + Fallback fallback) + { + var descriptor = AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened; + if (LinkedRegistrationOptions.IsRegistrationStrict(optionsProvider)) + { + descriptor = new DiagnosticDescriptor( + descriptor.Id, + descriptor.Title, + descriptor.MessageFormat, + descriptor.Category, + DiagnosticSeverity.Error, + descriptor.IsEnabledByDefault, + descriptor.Description, + descriptor.HelpLinkUri, + descriptor.CustomTags.ToArray()); + } + context.ReportDiagnostic(Diagnostic.Create( + descriptor, + fallback.Location, + fallback.Reason, + fallback.PackageId)); + } + + private sealed class UnitEdge + { + internal UnitEdge( + string packageId, + string sourceUnitId, + string targetUnitId, + LinkedUnitEdgeEvidenceKind evidenceKind) + { + PackageId = packageId; + SourceUnitId = sourceUnitId; + TargetUnitId = targetUnitId; + EvidenceKind = evidenceKind; + } + + internal string PackageId { get; } + internal string SourceUnitId { get; } + internal string TargetUnitId { get; } + internal LinkedUnitEdgeEvidenceKind EvidenceKind { get; } + } + + private sealed class UnitEdgeComparer : IEqualityComparer + { + internal static UnitEdgeComparer Instance { get; } = new(); + + public bool Equals(UnitEdge? x, UnitEdge? y) + { + return x is not null && y is not null && + x.PackageId == y.PackageId && + x.SourceUnitId == y.SourceUnitId && + x.TargetUnitId == y.TargetUnitId && + x.EvidenceKind == y.EvidenceKind; + } + + public int GetHashCode(UnitEdge obj) + { + unchecked + { + var hash = StringComparer.Ordinal.GetHashCode(obj.PackageId); + hash = (hash * 397) ^ StringComparer.Ordinal.GetHashCode(obj.SourceUnitId); + hash = (hash * 397) ^ StringComparer.Ordinal.GetHashCode(obj.TargetUnitId); + return (hash * 397) ^ (int)obj.EvidenceKind; + } + } + } + + private sealed class Fallback + { + internal Fallback( + string packageId, + string reason, + string source, + int line, + int column, + Location location) + { + PackageId = packageId; + Reason = reason; + Source = source; + Line = line; + Column = column; + Location = location; + } + + internal string PackageId { get; } + internal string Reason { get; } + internal string Source { get; } + internal int Line { get; } + internal int Column { get; } + internal Location Location { get; } + } + + private sealed class FallbackComparer : IEqualityComparer + { + internal static FallbackComparer Instance { get; } = new(); + + public bool Equals(Fallback? x, Fallback? y) + { + return x is not null && y is not null && + x.PackageId == y.PackageId && + x.Reason == y.Reason && + x.Source == y.Source && + x.Line == y.Line && + x.Column == y.Column; + } + + public int GetHashCode(Fallback obj) + { + unchecked + { + var hash = StringComparer.Ordinal.GetHashCode(obj.PackageId); + hash = (hash * 397) ^ StringComparer.Ordinal.GetHashCode(obj.Reason); + hash = (hash * 397) ^ StringComparer.Ordinal.GetHashCode(obj.Source); + hash = (hash * 397) ^ obj.Line; + return (hash * 397) ^ obj.Column; + } + } + } +} diff --git a/src/AtomUI.Generator.LinkedPublish/RegistrationUnitGraphPlanner.cs b/src/AtomUI.Generator.LinkedPublish/RegistrationUnitGraphPlanner.cs new file mode 100644 index 000000000..302721e97 --- /dev/null +++ b/src/AtomUI.Generator.LinkedPublish/RegistrationUnitGraphPlanner.cs @@ -0,0 +1,254 @@ +using System.Collections.Immutable; +using AtomUI.Generator.LinkedRegistration.Manifest; + +namespace AtomUI.Generator.LinkedRegistration; + +internal static class RegistrationUnitGraphPlanner +{ + internal static ImmutableArray Plan( + IReadOnlyList packageUnits, + IReadOnlyList packageEdges, + IEnumerable roots) + { + var unitsById = packageUnits.ToDictionary( + static unit => unit.UnitId, + StringComparer.Ordinal); + var dependencies = packageEdges.GroupBy(static edge => edge.SourceUnitId, StringComparer.Ordinal) + .ToDictionary( + static group => group.Key, + static group => (IReadOnlyList)group.Select(static edge => edge.TargetUnitId) + .Where(static unitId => unitId.Length != 0) + .Distinct(StringComparer.Ordinal) + .OrderBy(static unitId => unitId, StringComparer.Ordinal) + .ToArray(), + StringComparer.Ordinal); + var selected = SelectClosure(unitsById, dependencies, roots); + if (selected.Count == 0) + { + return ImmutableArray.Empty; + } + + var orderedDependencies = selected.ToDictionary( + unitId => unitId, + unitId => dependencies.TryGetValue(unitId, out var unitDependencies) + ? (IReadOnlyList)unitDependencies.Where(selected.Contains).ToArray() + : [], + StringComparer.Ordinal); + var components = FindStronglyConnectedComponents(selected, orderedDependencies); + var componentByUnit = new Dictionary(StringComparer.Ordinal); + for (var componentIndex = 0; componentIndex < components.Count; componentIndex++) + { + foreach (var unitId in components[componentIndex]) + { + componentByUnit.Add(unitId, componentIndex); + } + } + + var componentDependencies = new List>(components.Count); + var componentDependents = new List>(components.Count); + for (var index = 0; index < components.Count; index++) + { + componentDependencies.Add([]); + componentDependents.Add([]); + } + foreach (var edge in orderedDependencies) + { + var sourceComponent = componentByUnit[edge.Key]; + foreach (var targetUnitId in edge.Value) + { + var targetComponent = componentByUnit[targetUnitId]; + if (sourceComponent == targetComponent) + { + continue; + } + componentDependencies[sourceComponent].Add(targetComponent); + componentDependents[targetComponent].Add(sourceComponent); + } + } + + var componentOrderKeys = components.Select(component => component.Min( + unitId => unitsById[unitId].OrderKey)).ToArray(); + var componentNames = components.Select(GetMinUnitId).ToArray(); + var dependencyCount = componentDependencies.Select(static items => items.Count).ToArray(); + var ready = new SortedSet(Comparer.Create((left, right) => + { + if (left == right) + { + return 0; + } + var order = componentOrderKeys[left].CompareTo(componentOrderKeys[right]); + return order != 0 + ? order + : string.CompareOrdinal(componentNames[left], componentNames[right]); + })); + for (var componentIndex = 0; componentIndex < dependencyCount.Length; componentIndex++) + { + if (dependencyCount[componentIndex] == 0) + { + ready.Add(componentIndex); + } + } + + var orderedComponents = new List(components.Count); + while (ready.Count != 0) + { + var componentIndex = ready.Min; + ready.Remove(componentIndex); + orderedComponents.Add(componentIndex); + foreach (var dependent in componentDependents[componentIndex] + .OrderBy(index => componentOrderKeys[index]) + .ThenBy(index => componentNames[index], StringComparer.Ordinal)) + { + dependencyCount[dependent]--; + if (dependencyCount[dependent] == 0) + { + ready.Add(dependent); + } + } + } + + if (orderedComponents.Count != components.Count) + { + throw new InvalidOperationException("Registration Unit component graph did not converge."); + } + + var result = ImmutableArray.CreateBuilder(selected.Count); + foreach (var componentIndex in orderedComponents) + { + foreach (var unitId in components[componentIndex] + .OrderBy(id => unitsById[id].OrderKey) + .ThenBy(static id => id, StringComparer.Ordinal)) + { + result.Add(unitsById[unitId]); + } + } + return result.ToImmutable(); + } + + private static HashSet SelectClosure( + IReadOnlyDictionary unitsById, + IReadOnlyDictionary> dependencies, + IEnumerable roots) + { + var selected = new HashSet(StringComparer.Ordinal); + var pending = new Stack(roots.Where(unitsById.ContainsKey) + .Distinct(StringComparer.Ordinal) + .OrderByDescending(static unitId => unitId, StringComparer.Ordinal)); + while (pending.Count != 0) + { + var unitId = pending.Pop(); + if (!selected.Add(unitId) || !dependencies.TryGetValue(unitId, out var unitDependencies)) + { + continue; + } + for (var index = unitDependencies.Count - 1; index >= 0; index--) + { + var dependency = unitDependencies[index]; + if (unitsById.ContainsKey(dependency)) + { + pending.Push(dependency); + } + } + } + return selected; + } + + private static List> FindStronglyConnectedComponents( + ISet selected, + IReadOnlyDictionary> dependencies) + { + var reverseDependencies = selected.ToDictionary( + unitId => unitId, + static _ => new List(), + StringComparer.Ordinal); + foreach (var edge in dependencies) + { + foreach (var dependency in edge.Value) + { + reverseDependencies[dependency].Add(edge.Key); + } + } + foreach (var reverse in reverseDependencies.Values) + { + reverse.Sort(StringComparer.Ordinal); + } + + var visited = new HashSet(StringComparer.Ordinal); + var finishOrder = new List(selected.Count); + foreach (var root in selected.OrderBy(static unitId => unitId, StringComparer.Ordinal)) + { + if (!visited.Add(root)) + { + continue; + } + var frames = new Stack(); + frames.Push(new TraversalFrame(root, dependencies[root])); + while (frames.Count != 0) + { + var frame = frames.Pop(); + if (frame.NextIndex < frame.Neighbors.Count) + { + var neighbor = frame.Neighbors[frame.NextIndex]; + frame.NextIndex++; + frames.Push(frame); + if (visited.Add(neighbor)) + { + frames.Push(new TraversalFrame(neighbor, dependencies[neighbor])); + } + } + else + { + finishOrder.Add(frame.UnitId); + } + } + } + + visited.Clear(); + var components = new List>(); + for (var index = finishOrder.Count - 1; index >= 0; index--) + { + var root = finishOrder[index]; + if (!visited.Add(root)) + { + continue; + } + var component = new List(); + var pending = new Stack(); + pending.Push(root); + while (pending.Count != 0) + { + var unitId = pending.Pop(); + component.Add(unitId); + var neighbors = reverseDependencies[unitId]; + for (var neighborIndex = neighbors.Count - 1; neighborIndex >= 0; neighborIndex--) + { + var neighbor = neighbors[neighborIndex]; + if (visited.Add(neighbor)) + { + pending.Push(neighbor); + } + } + } + components.Add(component); + } + return components; + } + + private static string GetMinUnitId(IEnumerable unitIds) + { + return unitIds.OrderBy(static unitId => unitId, StringComparer.Ordinal).First(); + } + + private sealed class TraversalFrame + { + internal TraversalFrame(string unitId, IReadOnlyList neighbors) + { + UnitId = unitId; + Neighbors = neighbors; + } + + internal string UnitId { get; } + internal IReadOnlyList Neighbors { get; } + internal int NextIndex { get; set; } + } +} diff --git a/src/AtomUI.Generator/AtomUI.Generator.csproj b/src/AtomUI.Generator/AtomUI.Generator.csproj index 028b0f9a8..30ae8c5ff 100644 --- a/src/AtomUI.Generator/AtomUI.Generator.csproj +++ b/src/AtomUI.Generator/AtomUI.Generator.csproj @@ -28,6 +28,14 @@ + + + + + + + + + + + + + diff --git a/src/AtomUI.Generator/LinkedRegistration/ApplicationRegistrationPlanGenerator.cs b/src/AtomUI.Generator/LinkedRegistration/ApplicationRegistrationPlanGenerator.cs index 6a00ca8c4..902771b4c 100644 --- a/src/AtomUI.Generator/LinkedRegistration/ApplicationRegistrationPlanGenerator.cs +++ b/src/AtomUI.Generator/LinkedRegistration/ApplicationRegistrationPlanGenerator.cs @@ -3,6 +3,7 @@ using AtomUI.Generator.Diagnostics; using AtomUI.Generator.LinkedRegistration.Manifest; using AtomUI.Generator.LinkedRegistration.Model; using AtomUI.Generator.LinkedRegistration.Writers; +using AtomUI.LinkedRegistration.Protocol; using Microsoft.CodeAnalysis; namespace AtomUI.Generator.LinkedRegistration; @@ -12,14 +13,17 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { + var csharpCandidates = LinkedCSharpUsageCandidateProvider.Create(context).Collect(); var input = context.CompilationProvider + .Combine(csharpCandidates) .Combine(context.AdditionalTextsProvider.Collect()) .Combine(context.AnalyzerConfigOptionsProvider); context.RegisterSourceOutput(input, static (productionContext, value) => { Generate( productionContext, - value.Left.Left, + value.Left.Left.Left, + value.Left.Left.Right, value.Left.Right, value.Right); }); @@ -28,6 +32,7 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator private static void Generate( SourceProductionContext context, Compilation compilation, + ImmutableArray csharpCandidates, ImmutableArray additionalTexts, Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptionsProvider optionsProvider) { @@ -38,7 +43,11 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator return; } - var catalog = LinkedRegistrationManifestCatalog.Create(compilation, static _ => { }); + var catalog = LinkedRegistrationManifestCatalog.Create( + compilation, + additionalTexts, + optionsProvider, + context.ReportDiagnostic); if (catalog.PlanOwners.Count != 0) { context.ReportDiagnostic(Diagnostic.Create( @@ -56,10 +65,17 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator var currentUsages = LinkedRegistrationUsageGenerator.CollectApplicationUsages( compilation, catalog, + csharpCandidates, additionalTexts, optionsProvider, context.CancellationToken, - diagnostic => hasUsageErrors |= diagnostic.Severity == DiagnosticSeverity.Error); + diagnostic => hasUsageErrors |= diagnostic.Severity == DiagnosticSeverity.Error, + out var applicationBudgetExceeded); + if (csharpCandidates.Length > LinkedRegistrationAnalysisBudget.MaxSourceCandidates || + applicationBudgetExceeded) + { + catalog.AddAnalysisBudgetFallback(compilation.AssemblyName ?? ""); + } if (hasUsageErrors) { return; @@ -87,12 +103,22 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator var packageUnits = catalog.Units.Values.Where(unit => string.Equals(unit.PackageId, packageId, StringComparison.Ordinal)) - .OrderBy(static unit => unit.UnitId, StringComparer.Ordinal) + .OrderBy(static unit => unit.OrderKey) + .ThenBy(static unit => unit.UnitId, StringComparer.Ordinal) .ToArray(); - var isLegacy = packageUnits.Length == 0; + var hasPackageFallback = usages.Any(usage => + 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)); + var isLegacy = packageUnits.Length == 0 && !hasPackageFallback; var useFullFallback = isLegacy || usages.Any(usage => usage.Kind == LinkedUsageKind.PackageRoot && - string.Equals(usage.Identity, packageId, StringComparison.Ordinal)); + string.Equals(usage.Identity, packageId, StringComparison.Ordinal)) || + catalog.Fallbacks.Any(fallback => + fallback.PackageId.Length == 0 || + string.Equals(fallback.PackageId, packageId, StringComparison.Ordinal)); if (isLegacy) { if (ValidateFragmentMethod( @@ -118,6 +144,14 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator var selectedUnitIds = new HashSet(StringComparer.Ordinal); if (!useFullFallback) { + if (string.Equals(package.Granularity, "Package", StringComparison.Ordinal)) + { + selectedUnitIds.UnionWith(packageUnits.Select(static unit => unit.UnitId)); + } + if (catalog.RootUnitsByPackage.TryGetValue(packageId, out var rootUnits)) + { + selectedUnitIds.UnionWith(rootUnits); + } foreach (var usage in usages) { if (usage.Kind == LinkedUsageKind.Control && @@ -135,8 +169,15 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator } } - var selectedUnits = packageUnits.Where(unit => selectedUnitIds.Contains(unit.UnitId)) - .ToImmutableArray(); + var selectedUnits = useFullFallback + ? ImmutableArray.Empty + : RegistrationUnitGraphPlanner.Plan( + packageUnits, + catalog.UnitEdges.Where(edge => string.Equals( + edge.PackageId, + packageId, + StringComparison.Ordinal)).ToArray(), + selectedUnitIds); if (useFullFallback && !isLegacy && !ValidateFragmentMethod( compilation, @@ -183,6 +224,24 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator package)); } + foreach (var fallback in catalog.Fallbacks.OrderBy( + static item => item.PackageId, + StringComparer.Ordinal).ThenBy(static item => item.Source, StringComparer.Ordinal) + .ThenBy(static item => item.Line) + .ThenBy(static item => item.Column) + .ThenBy(static item => item.Reason, StringComparer.Ordinal)) + { + if (fallback.PackageId.Length != 0 && !invokedPackageSet.Contains(fallback.PackageId)) + { + continue; + } + context.ReportDiagnostic(Diagnostic.Create( + GetFallbackDescriptor(LinkedRegistrationOptions.IsRegistrationStrict(optionsProvider)), + Location.None, + fallback.Reason, + fallback.PackageId.Length == 0 ? "" : fallback.PackageId)); + } + if (hasInvalidCallTarget) { return; @@ -320,6 +379,25 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator descriptor.CustomTags.ToArray()); } + private static DiagnosticDescriptor GetFallbackDescriptor(bool strict) + { + if (!strict) + { + return AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened; + } + var descriptor = AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened; + return new DiagnosticDescriptor( + descriptor.Id, + descriptor.Title, + descriptor.MessageFormat, + descriptor.Category, + DiagnosticSeverity.Error, + descriptor.IsEnabledByDefault, + descriptor.Description, + descriptor.HelpLinkUri, + descriptor.CustomTags.ToArray()); + } + private enum FragmentMethodKind { PackageBuilder, diff --git a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationManifestCatalog.cs b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationManifestCatalog.cs index 05d9ce7f3..32e979732 100644 --- a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationManifestCatalog.cs +++ b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationManifestCatalog.cs @@ -1,7 +1,11 @@ +using System.Collections.Immutable; +using System.Text; using AtomUI.Generator.Diagnostics; using AtomUI.Generator.LinkedRegistration.Manifest; using AtomUI.Generator.LinkedRegistration.Model; +using AtomUI.LinkedRegistration.Protocol; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; namespace AtomUI.Generator.LinkedRegistration; @@ -9,10 +13,19 @@ internal sealed class LinkedRegistrationManifestCatalog { private const string AssemblyMetadataAttribute = "System.Reflection.AssemblyMetadataAttribute"; + private const string SidecarMetadata = + "build_metadata.AdditionalFiles.AtomUILinkedSidecar"; private readonly Action _reportDiagnostic; private readonly Dictionary _packageByAssembly = new(SymbolEqualityComparer.Default); private readonly List _referencedUsagesToValidate = []; + private readonly HashSet _referencedUsageValidationKeys = new(StringComparer.Ordinal); + private readonly HashSet _referencedUsageKeys = new(StringComparer.Ordinal); + private readonly HashSet _unitEdgeSet = []; + private readonly HashSet _fallbackSet = []; + private readonly HashSet _sidecarAssemblies = new(StringComparer.Ordinal); + private long _totalSidecarBytes; + private bool _fineAnalysisDisabled; private LinkedRegistrationManifestCatalog(Action reportDiagnostic) { @@ -29,6 +42,10 @@ internal sealed class LinkedRegistrationManifestCatalog new(StringComparer.Ordinal); internal Dictionary PackageByUnit { get; } = new(StringComparer.Ordinal); + internal List UnitEdges { get; } = []; + internal Dictionary> RootUnitsByPackage { get; } = + new(StringComparer.Ordinal); + internal List Fallbacks { get; } = []; internal List ReferencedUsages { get; } = []; internal Dictionary> XmlNamespaces { get; } = new(StringComparer.Ordinal); @@ -40,8 +57,34 @@ internal sealed class LinkedRegistrationManifestCatalog internal static LinkedRegistrationManifestCatalog Create( Compilation compilation, Action reportDiagnostic) + { + return Create( + compilation, + ImmutableArray.Empty, + optionsProvider: null, + reportDiagnostic); + } + + internal static LinkedRegistrationManifestCatalog Create( + Compilation compilation, + ImmutableArray additionalTexts, + AnalyzerConfigOptionsProvider? optionsProvider, + Action reportDiagnostic) { var catalog = new LinkedRegistrationManifestCatalog(reportDiagnostic); + if (optionsProvider is not null) + { + foreach (var text in additionalTexts.OrderBy( + static item => item.Path, + StringComparer.Ordinal)) + { + if (optionsProvider.GetOptions(text).TryGetValue(SidecarMetadata, out var enabled) && + string.Equals(enabled, "true", StringComparison.OrdinalIgnoreCase)) + { + catalog.ReadSidecar(text); + } + } + } foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols.OrderBy( static item => item.Identity.Name, StringComparer.Ordinal)) @@ -53,6 +96,219 @@ internal sealed class LinkedRegistrationManifestCatalog return catalog; } + private void ReadSidecar(AdditionalText text) + { + var sourceText = text.GetText(); + if (sourceText is null || sourceText.Length == 0) + { + ReportIncompatibleInput(text.Path, "sidecar is empty"); + return; + } + if (sourceText.Length > LinkedRegistrationAnalysisBudget.MaxSidecarBytes) + { + DisableFineAnalysis(text.Path); + return; + } + + var content = sourceText.ToString(); + var bytes = Encoding.UTF8.GetBytes(content); + if (bytes.Length > LinkedRegistrationAnalysisBudget.MaxSidecarBytes || + _totalSidecarBytes + bytes.Length > LinkedRegistrationAnalysisBudget.MaxTotalSidecarBytes) + { + DisableFineAnalysis(text.Path); + return; + } + _totalSidecarBytes += bytes.Length; + var error = string.Empty; + if (string.IsNullOrWhiteSpace(content) || + !LinkedRegistrationSidecarCodec.TryRead( + bytes, + out var sidecar, + out error)) + { + ReportIncompatibleInput(text.Path, error.Length == 0 ? "sidecar is empty" : error); + return; + } + if (!_sidecarAssemblies.Add(sidecar!.Assembly.Name)) + { + ReportInvalidManifest( + sidecar.Assembly.Name, + "more than one linked-registration Sidecar declares the same assembly"); + return; + } + + if (_fineAnalysisDisabled || ExceedsStructuralBudget(sidecar!)) + { + ReadBudgetFallbackSidecar(sidecar!, text.Path); + return; + } + + var isControlPackage = sidecar.Packages.Length != 0; + var ownPackageIds = new HashSet( + sidecar.Packages.Select(static package => package.Id), + StringComparer.Ordinal); + foreach (var package in sidecar.Packages) + { + AddRecord( + new LinkedPackageManifestRecord( + package.Id, + package.AssemblyName, + package.Granularity, + string.Join(";", package.EntryMethods), + package.FullFragment.Type, + package.FullFragment.Method, + package.SharedFragment?.Type, + package.SharedFragment?.Method), + includeUsages: false, + validateUsage: false); + foreach (var unit in package.Units) + { + AddRecord( + new LinkedUnitManifestRecord( + package.Id, + unit.Id, + unit.FragmentType, + unit.FragmentMethod, + unit.OrderKey), + includeUsages: false, + validateUsage: false); + foreach (var control in unit.Controls) + { + AddRecord( + new LinkedControlMapManifestRecord(package.Id, control, unit.Id), + includeUsages: false, + validateUsage: false); + } + } + foreach (var edge in package.UnitEdges) + { + if (!Enum.TryParse( + edge.EvidenceKind, + ignoreCase: false, + out LinkedUnitEdgeEvidenceKind evidenceKind)) + { + ReportIncompatibleInput( + text.Path, + $"unknown UnitEdge evidence kind '{edge.EvidenceKind}'"); + continue; + } + AddRecord( + new LinkedUnitEdgeManifestRecord( + package.Id, + edge.SourceUnitId, + edge.TargetUnitId, + evidenceKind), + includeUsages: false, + validateUsage: false); + } + foreach (var rootUnit in package.RootUnits) + { + AddRecord( + new LinkedRootUnitManifestRecord(package.Id, rootUnit), + includeUsages: false, + validateUsage: false); + } + } + foreach (var usage in sidecar.Usages) + { + if (!Enum.TryParse(usage.Kind, ignoreCase: false, out LinkedUsageKind kind)) + { + ReportIncompatibleInput(text.Path, $"unknown Usage kind '{usage.Kind}'"); + continue; + } + var includeUsage = !isControlPackage || + (kind == LinkedUsageKind.PackageRoot && + ownPackageIds.Contains(usage.Identity)); + AddRecord( + new LinkedUsageManifestRecord( + kind, + usage.Identity, + usage.Source, + usage.Line, + usage.Column), + includeUsage, + validateUsage: true); + } + foreach (var fallback in sidecar.Fallbacks) + { + AddRecord( + new LinkedFallbackManifestRecord( + fallback.PackageId, + fallback.Reason, + fallback.Source, + fallback.Line, + fallback.Column), + includeUsages: false, + validateUsage: false); + } + } + + private bool ExceedsStructuralBudget(LinkedRegistrationSidecar sidecar) + { + long unitCount = 0; + long edgeCount = 0; + long rootCount = 0; + long controlCount = 0; + foreach (var package in sidecar.Packages) + { + unitCount += package.Units.Length; + edgeCount += package.UnitEdges.Length; + rootCount += package.RootUnits.Length; + foreach (var unit in package.Units) + { + controlCount += unit.Controls.Length; + } + } + + return Packages.Count + (long)sidecar.Packages.Length > LinkedRegistrationAnalysisBudget.MaxPackages || + Units.Count + unitCount > LinkedRegistrationAnalysisBudget.MaxRegistrationUnits || + UnitEdges.Count + edgeCount > LinkedRegistrationAnalysisBudget.MaxUnitEdges || + RootUnitsByPackage.Sum(static item => item.Value.Count) + rootCount > + LinkedRegistrationAnalysisBudget.MaxRootUnits || + ControlMaps.Count + controlCount > LinkedRegistrationAnalysisBudget.MaxControlMappings || + ReferencedUsages.Count + (long)sidecar.Usages.Length > + LinkedRegistrationAnalysisBudget.MaxUsages || + Fallbacks.Count + (long)sidecar.Fallbacks.Length > + LinkedRegistrationAnalysisBudget.MaxFallbacks; + } + + private void ReadBudgetFallbackSidecar(LinkedRegistrationSidecar sidecar, string source) + { + foreach (var package in sidecar.Packages) + { + AddRecord( + new LinkedPackageManifestRecord( + package.Id, + package.AssemblyName, + package.Granularity, + string.Join(";", package.EntryMethods), + package.FullFragment.Type, + package.FullFragment.Method, + package.SharedFragment?.Type, + package.SharedFragment?.Method), + includeUsages: false, + validateUsage: false); + } + foreach (var usage in sidecar.Usages) + { + if (!Enum.TryParse(usage.Kind, ignoreCase: false, out LinkedUsageKind kind) || + kind is not (LinkedUsageKind.Entry or LinkedUsageKind.PackageRoot)) + { + continue; + } + AddRecord( + new LinkedUsageManifestRecord( + kind, + usage.Identity, + usage.Source, + usage.Line, + usage.Column), + includeUsages: true, + validateUsage: true); + } + DisableFineAnalysis(source); + } + internal bool TryGetPackageId(IAssemblySymbol assembly, out string packageId) { return _packageByAssembly.TryGetValue(assembly, out packageId!); @@ -84,6 +340,10 @@ internal sealed class LinkedRegistrationManifestCatalog } continue; } + if (_sidecarAssemblies.Contains(assembly.Identity.Name)) + { + continue; + } if (!IsSupportedManifestKey(key)) { ReportIncompatibleInput(assembly.Identity.Name, $"unsupported manifest key '{key}'"); @@ -124,6 +384,10 @@ internal sealed class LinkedRegistrationManifestCatalog : null; foreach (var record in records) { + if (packageRecords.Length != 0 && record is not LinkedPackageManifestRecord) + { + continue; + } var includeReferencedUsage = includeUsages && (!isControlPackage || record is LinkedUsageManifestRecord @@ -139,7 +403,26 @@ internal sealed class LinkedRegistrationManifestCatalog includeReferencedUsage, validateUsage: includeUsages); } - var assemblyPackages = packageRecords.Select(static package => package.PackageId) + if (includeUsages && packageRecords.Length != 0) + { + foreach (var package in packageRecords) + { + AddRecord( + new LinkedFallbackManifestRecord( + package.PackageId, + "MissingSidecar", + assembly.Identity.Name, + 0, + 0), + includeUsages: false, + validateUsage: false); + } + } + var assemblyPackages = Packages.Values.Where(package => string.Equals( + package.AssemblyName, + assembly.Identity.Name, + StringComparison.Ordinal)) + .Select(static package => package.PackageId) .Distinct(StringComparer.Ordinal) .ToArray(); if (assemblyPackages.Length == 1 && Packages.ContainsKey(assemblyPackages[0])) @@ -168,6 +451,15 @@ internal sealed class LinkedRegistrationManifestCatalog bool includeUsages, bool validateUsage) { + if (_fineAnalysisDisabled && record is not LinkedPackageManifestRecord && + record is not LinkedUsageManifestRecord + { + Kind: LinkedUsageKind.Entry or LinkedUsageKind.PackageRoot + }) + { + return; + } + switch (record) { case LinkedPackageManifestRecord package: @@ -186,14 +478,49 @@ internal sealed class LinkedRegistrationManifestCatalog } break; case LinkedUnitManifestRecord unit: + if (Units.Count >= LinkedRegistrationAnalysisBudget.MaxRegistrationUnits) + { + DisableFineAnalysis(unit.PackageId); + break; + } if (TryAddRecord(Units, unit.UnitId, unit, "Registration Unit")) { PackageByUnit[unit.UnitId] = unit.PackageId; } break; case LinkedControlMapManifestRecord controlMap: + if (ControlMaps.Count >= LinkedRegistrationAnalysisBudget.MaxControlMappings) + { + DisableFineAnalysis(controlMap.PackageId); + break; + } TryAddRecord(ControlMaps, controlMap.MetadataName, controlMap, "ControlMap"); break; + case LinkedUnitEdgeManifestRecord edge: + if (UnitEdges.Count >= LinkedRegistrationAnalysisBudget.MaxUnitEdges) + { + DisableFineAnalysis(edge.PackageId); + break; + } + if (_unitEdgeSet.Add(edge)) + { + UnitEdges.Add(edge); + } + break; + case LinkedRootUnitManifestRecord rootUnit: + if (RootUnitsByPackage.Sum(static item => item.Value.Count) >= + LinkedRegistrationAnalysisBudget.MaxRootUnits) + { + DisableFineAnalysis(rootUnit.PackageId); + break; + } + if (!RootUnitsByPackage.TryGetValue(rootUnit.PackageId, out var rootUnits)) + { + rootUnits = new HashSet(StringComparer.Ordinal); + RootUnitsByPackage.Add(rootUnit.PackageId, rootUnits); + } + rootUnits.Add(rootUnit.UnitId); + break; case LinkedUsageManifestRecord usage: var usageInfo = new LinkedUsageInfo( usage.Kind, @@ -203,15 +530,32 @@ internal sealed class LinkedRegistrationManifestCatalog usage.Column, string.Empty, null); - if (validateUsage && !ContainsUsage(_referencedUsagesToValidate, usageInfo)) + var usageKey = GetUsageKey(usageInfo); + if (validateUsage && _referencedUsageValidationKeys.Add(usageKey)) { _referencedUsagesToValidate.Add(usageInfo); } - if (includeUsages && !ContainsUsage(ReferencedUsages, usageInfo)) + if (includeUsages && _referencedUsageKeys.Add(usageKey)) { + if (ReferencedUsages.Count >= LinkedRegistrationAnalysisBudget.MaxUsages) + { + DisableFineAnalysis(usage.Identity); + break; + } ReferencedUsages.Add(usageInfo); } break; + case LinkedFallbackManifestRecord fallback: + if (_fallbackSet.Add(fallback)) + { + if (Fallbacks.Count >= LinkedRegistrationAnalysisBudget.MaxFallbacks) + { + DisableFineAnalysis(fallback.PackageId); + break; + } + Fallbacks.Add(fallback); + } + break; } } @@ -264,18 +608,51 @@ internal sealed class LinkedRegistrationManifestCatalog $"Usage '{usage.Kind}' references an unknown identity '{usage.Identity}'"); } } + foreach (var edge in UnitEdges) + { + if (!Units.TryGetValue(edge.SourceUnitId, out var sourceUnit) || + !Units.TryGetValue(edge.TargetUnitId, out var targetUnit) || + !string.Equals(sourceUnit.PackageId, edge.PackageId, StringComparison.Ordinal) || + !string.Equals(targetUnit.PackageId, edge.PackageId, StringComparison.Ordinal)) + { + ReportInvalidManifest( + edge.PackageId, + $"UnitEdge '{edge.SourceUnitId}' -> '{edge.TargetUnitId}' does not resolve within its Package"); + } + } + foreach (var packageRoots in RootUnitsByPackage) + { + foreach (var unitId in packageRoots.Value) + { + if (!Units.TryGetValue(unitId, out var unit) || + !string.Equals(unit.PackageId, packageRoots.Key, StringComparison.Ordinal)) + { + ReportInvalidManifest( + packageRoots.Key, + $"Package root references unknown Registration Unit '{unitId}'"); + } + } + } + foreach (var fallback in Fallbacks) + { + if (fallback.PackageId.Length != 0 && !Packages.ContainsKey(fallback.PackageId)) + { + ReportInvalidManifest( + fallback.PackageId, + $"Fallback '{fallback.Reason}' references an unknown Package"); + } + } } - private static bool ContainsUsage( - IEnumerable usages, - LinkedUsageInfo candidate) + private static string GetUsageKey(LinkedUsageInfo usage) { - return usages.Any(existing => - existing.Kind == candidate.Kind && - existing.Identity == candidate.Identity && - existing.Source == candidate.Source && - existing.Line == candidate.Line && - existing.Column == candidate.Column); + return string.Join( + "\u001f", + usage.Kind, + usage.Identity, + usage.Source, + usage.Line.ToString(System.Globalization.CultureInfo.InvariantCulture), + usage.Column.ToString(System.Globalization.CultureInfo.InvariantCulture)); } private static IEnumerable SplitEntryMethodMetadataNames(string entries) @@ -317,6 +694,26 @@ internal sealed class LinkedRegistrationManifestCatalog detail)); } + internal void AddAnalysisBudgetFallback(string source) + { + DisableFineAnalysis(source); + } + + private void DisableFineAnalysis(string source) + { + _fineAnalysisDisabled = true; + var fallback = new LinkedFallbackManifestRecord( + string.Empty, + "AnalysisBudgetExceeded", + source, + 0, + 0); + if (_fallbackSet.Add(fallback)) + { + Fallbacks.Add(fallback); + } + } + private void ReportIncompatibleInput(string input, string detail) { HasErrors = true; @@ -332,6 +729,9 @@ internal sealed class LinkedRegistrationManifestCatalog return key == LinkedRegistrationProtocol.PackageManifestKey || key == LinkedRegistrationProtocol.UnitManifestKey || key == LinkedRegistrationProtocol.ControlMapManifestKey || - key == LinkedRegistrationProtocol.UsageManifestKey; + key == LinkedRegistrationProtocol.UnitEdgeManifestKey || + key == LinkedRegistrationProtocol.RootUnitManifestKey || + key == LinkedRegistrationProtocol.UsageManifestKey || + key == LinkedRegistrationProtocol.FallbackManifestKey; } } diff --git a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationProtocol.cs b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationProtocol.cs index 411a67688..82c5db5d5 100644 --- a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationProtocol.cs +++ b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationProtocol.cs @@ -7,6 +7,9 @@ internal static class LinkedRegistrationProtocol internal const string PackageManifestKey = MetadataPrefix + "Package.v1"; internal const string UnitManifestKey = MetadataPrefix + "Unit.v1"; internal const string ControlMapManifestKey = MetadataPrefix + "ControlMap.v1"; + internal const string UnitEdgeManifestKey = MetadataPrefix + "UnitEdge.v1"; + internal const string RootUnitManifestKey = MetadataPrefix + "RootUnit.v1"; internal const string UsageManifestKey = MetadataPrefix + "Usage.v1"; + internal const string FallbackManifestKey = MetadataPrefix + "Fallback.v1"; internal const string PlanMarkerKey = MetadataPrefix + "Plan.v1"; } diff --git a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationUsageGenerator.cs b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationUsageGenerator.cs index f7329390f..87829e279 100644 --- a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationUsageGenerator.cs +++ b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationUsageGenerator.cs @@ -5,6 +5,7 @@ using System.Xml.Linq; using AtomUI.Generator.Diagnostics; using AtomUI.Generator.LinkedRegistration.Manifest; using AtomUI.Generator.LinkedRegistration.Model; +using AtomUI.LinkedRegistration.Protocol; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; @@ -20,14 +21,17 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator "build_metadata.AdditionalFiles.AtomUIAxamlUsage"; public void Initialize(IncrementalGeneratorInitializationContext context) { + var csharpCandidates = LinkedCSharpUsageCandidateProvider.Create(context).Collect(); var input = context.CompilationProvider + .Combine(csharpCandidates) .Combine(context.AdditionalTextsProvider.Collect()) .Combine(context.AnalyzerConfigOptionsProvider); context.RegisterSourceOutput(input, static (productionContext, value) => { Generate( productionContext, - value.Left.Left, + value.Left.Left.Left, + value.Left.Left.Right, value.Left.Right, value.Right); }); @@ -36,10 +40,15 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator private static void Generate( SourceProductionContext context, Compilation compilation, + ImmutableArray csharpCandidates, ImmutableArray additionalTexts, AnalyzerConfigOptionsProvider optionsProvider) { - var catalog = LinkedRegistrationManifestCatalog.Create(compilation, context.ReportDiagnostic); + var catalog = LinkedRegistrationManifestCatalog.Create( + compilation, + additionalTexts, + optionsProvider, + context.ReportDiagnostic); if (LinkedRegistrationOptions.GetDeclaredPackageId(optionsProvider).Length != 0) { ValidateMarkedAxamlInputs( @@ -61,7 +70,7 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator LinkedRegistrationOptions.GetProjectDirectory(optionsProvider), context.ReportDiagnostic); - collector.CollectCSharp(context.CancellationToken); + collector.CollectCSharp(csharpCandidates, context.CancellationToken); foreach (var text in additionalTexts.OrderBy(static item => item.Path, StringComparer.Ordinal)) { if (optionsProvider.GetOptions(text).TryGetValue(AxamlUsageMetadata, out var enabled) && @@ -85,16 +94,29 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator { LinkedRegistrationMetadataWriter.Write(source, usage.ToManifestRecord()); } + if (collector.AnalysisBudgetExceeded) + { + LinkedRegistrationMetadataWriter.Write( + source, + new LinkedFallbackManifestRecord( + string.Empty, + "AnalysisBudgetExceeded", + compilation.AssemblyName ?? "", + 0, + 0)); + } context.AddSource("LinkedRegistrationUsage.g.cs", GeneratedSourceText.From(source.ToString())); } internal static IReadOnlyList CollectApplicationUsages( Compilation compilation, LinkedRegistrationManifestCatalog catalog, + ImmutableArray csharpCandidates, ImmutableArray additionalTexts, AnalyzerConfigOptionsProvider optionsProvider, CancellationToken cancellationToken, - Action reportDiagnostic) + Action reportDiagnostic, + out bool analysisBudgetExceeded) { var collector = new UsageCollector( compilation, @@ -104,7 +126,7 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator LinkedRegistrationOptions.IsRegistrationStrict(optionsProvider), LinkedRegistrationOptions.GetProjectDirectory(optionsProvider), reportDiagnostic); - collector.CollectCSharp(cancellationToken); + collector.CollectCSharp(csharpCandidates, cancellationToken); foreach (var text in additionalTexts.OrderBy(static item => item.Path, StringComparer.Ordinal)) { if (optionsProvider.GetOptions(text).TryGetValue(AxamlUsageMetadata, out var enabled) && @@ -114,6 +136,7 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator } } collector.CompleteFallbacks(); + analysisBudgetExceeded = collector.AnalysisBudgetExceeded; return collector.GetOrderedUsages(); } @@ -161,6 +184,9 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator private readonly HashSet _explicitPackageRoots = new(StringComparer.Ordinal); private readonly Dictionary> _axamlPackagesBySource = new(StringComparer.Ordinal); + private int _axamlCandidateCount; + + internal bool AnalysisBudgetExceeded { get; private set; } internal UsageCollector( Compilation compilation, @@ -178,47 +204,63 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator _reportDiagnostic = reportDiagnostic; } - internal void CollectCSharp(CancellationToken cancellationToken) + internal void CollectCSharp( + IReadOnlyList candidates, + CancellationToken cancellationToken) { - CollectNamespace(_compilation.Assembly.GlobalNamespace); - foreach (var tree in _compilation.SyntaxTrees.OrderBy( - static item => item.FilePath, - StringComparer.Ordinal)) + if (candidates.Count > LinkedRegistrationAnalysisBudget.MaxSourceCandidates) + { + AnalysisBudgetExceeded = true; + foreach (var candidate in candidates) + { + cancellationToken.ThrowIfCancellationRequested(); + CollectBudgetCandidate(candidate); + } + return; + } + + foreach (var candidate in candidates.OrderBy( + static item => item.Source, + StringComparer.Ordinal).ThenBy(static item => item.SpanStart) + .ThenBy(static item => item.Kind) + .ThenBy(static item => item.Identity, StringComparer.Ordinal)) { cancellationToken.ThrowIfCancellationRequested(); - var model = _compilation.GetSemanticModel(tree); - foreach (var node in tree.GetRoot(cancellationToken).DescendantNodes()) + var location = candidate.CreateLocation(); + switch (candidate.Kind) { - switch (node) - { - case ObjectCreationExpressionSyntax objectCreation - when model.GetOperation(objectCreation, cancellationToken) is - IObjectCreationOperation operation: - AddType(operation.Type, operation.Syntax.GetLocation()); - break; - case ImplicitObjectCreationExpressionSyntax implicitCreation - when model.GetOperation(implicitCreation, cancellationToken) is - IObjectCreationOperation operation: - AddType(operation.Type, operation.Syntax.GetLocation()); - break; - case TypeOfExpressionSyntax typeOfExpression - when model.GetOperation(typeOfExpression, cancellationToken) is - ITypeOfOperation operation: - AddType(operation.TypeOperand, operation.Syntax.GetLocation()); - break; - case InvocationExpressionSyntax invocationExpression - when model.GetOperation(invocationExpression, cancellationToken) is - IInvocationOperation operation: - CollectInvocation(operation); - break; - } + case LinkedCSharpUsageCandidateKind.Type: + AddType(candidate.Identity, location); + break; + case LinkedCSharpUsageCandidateKind.Call: + CollectInvocation(candidate, location); + break; + case LinkedCSharpUsageCandidateKind.Dynamic: + AddDynamicUse(candidate.Identity, location); + break; } } } internal void CollectAxaml(AdditionalText text, CancellationToken cancellationToken) { - var content = text.GetText(cancellationToken)?.ToString(); + var sourceText = text.GetText(cancellationToken); + if (sourceText is null || sourceText.Length == 0) + { + ReportProtocolError(text.Path, "marked AXAML usage input is empty"); + return; + } + if (sourceText.Length > LinkedRegistrationAnalysisBudget.MaxAxamlUsageBytes) + { + AnalysisBudgetExceeded = true; + return; + } + var content = sourceText.ToString(); + if (Encoding.UTF8.GetByteCount(content) > LinkedRegistrationAnalysisBudget.MaxAxamlUsageBytes) + { + AnalysisBudgetExceeded = true; + return; + } if (string.IsNullOrWhiteSpace(content)) { ReportProtocolError(text.Path, "marked AXAML usage input is empty"); @@ -231,7 +273,16 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator return; } - var elements = root!.Elements().ToArray(); + var elements = root!.Elements() + .Take(LinkedRegistrationAnalysisBudget.MaxAxamlCandidates + 1) + .ToArray(); + _axamlCandidateCount += elements.Length; + if (elements.Length > LinkedRegistrationAnalysisBudget.MaxAxamlCandidates || + _axamlCandidateCount > LinkedRegistrationAnalysisBudget.MaxAxamlCandidates) + { + AnalysisBudgetExceeded = true; + return; + } foreach (var element in elements.Where(IsExplicitRootUsage)) { CollectAxamlUsage(element); @@ -311,26 +362,40 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator internal void CompleteFallbacks() { + if (AnalysisBudgetExceeded) + { + return; + } + foreach (var dynamicUse in _dynamicUses.OrderBy( static item => item.Source, StringComparer.Ordinal).ThenBy(static item => item.Line) .ThenBy(static item => item.Column)) { - if (_explicitPackageRoots.Count != 1) + var packages = _explicitPackageRoots.Count != 0 + ? _explicitPackageRoots + : _invokedPackages; + if (packages.Count == 0) { - ReportInvalidScope("Package", dynamicUse.Identity, dynamicUse.Location); + ReportInvalidScope( + "Package", + dynamicUse.Identity, + dynamicUse.Location); continue; } - - var packageId = _explicitPackageRoots.Single(); - AddPackageFallback( - packageId, - dynamicUse.Source, - dynamicUse.Line, - dynamicUse.Column, - dynamicUse.Location, - AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened, - dynamicUse.Identity); + foreach (var packageId in packages.OrderBy( + static packageId => packageId, + StringComparer.Ordinal)) + { + AddPackageFallback( + packageId, + dynamicUse.Source, + dynamicUse.Line, + dynamicUse.Column, + dynamicUse.Location, + AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened, + dynamicUse.Identity); + } } foreach (var uncertainty in _axamlUncertainties.OrderBy( @@ -414,68 +479,28 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator .ToArray(); } - private void CollectNamespace(INamespaceSymbol namespaceSymbol) + private void CollectInvocation( + LinkedCSharpUsageCandidate invocation, + Location location) { - foreach (var type in namespaceSymbol.GetTypeMembers().OrderBy( - static item => item.MetadataName, - StringComparer.Ordinal)) + if (_catalog.ControlMaps.TryGetValue( + invocation.ContainingTypeIdentity, + out var controlMap)) { - CollectNamedType(type); + var sourceLocation = GetLocationInfo(location); + AddControlUsage( + controlMap, + sourceLocation.Source, + sourceLocation.Line, + sourceLocation.Column, + location); } - foreach (var child in namespaceSymbol.GetNamespaceMembers().OrderBy( - static item => item.Name, - StringComparer.Ordinal)) - { - CollectNamespace(child); - } - } - private void CollectNamedType(INamedTypeSymbol type) - { - var location = GetSourceLocation(type); - AddType(type.BaseType, location); - foreach (var interfaceType in type.Interfaces) + if (_catalog.PackageIdsByEntry.TryGetValue( + invocation.Identity, + out var entryPackageIds)) { - AddType(interfaceType, location); - } - foreach (var member in type.GetMembers()) - { - switch (member) - { - case IFieldSymbol field: - AddType(field.Type, GetSourceLocation(field)); - break; - case IPropertySymbol property: - AddType(property.Type, GetSourceLocation(property)); - foreach (var parameter in property.Parameters) - { - AddType(parameter.Type, GetSourceLocation(parameter)); - } - break; - case IEventSymbol eventSymbol: - AddType(eventSymbol.Type, GetSourceLocation(eventSymbol)); - break; - case IMethodSymbol method: - AddType(method.ReturnType, GetSourceLocation(method)); - foreach (var parameter in method.Parameters) - { - AddType(parameter.Type, GetSourceLocation(parameter)); - } - break; - case INamedTypeSymbol nested: - CollectNamedType(nested); - break; - } - } - } - - private void CollectInvocation(IInvocationOperation invocation) - { - var target = invocation.TargetMethod.OriginalDefinition; - var methodName = LinkedRegistrationSymbolName.GetMethodMetadataName(target); - if (_catalog.PackageIdsByEntry.TryGetValue(methodName, out var entryPackageIds)) - { - var location = GetLocationInfo(invocation.Syntax.GetLocation()); + var sourceLocation = GetLocationInfo(location); foreach (var packageId in entryPackageIds.OrderBy( static packageId => packageId, StringComparer.Ordinal)) @@ -484,97 +509,76 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator AddUsage(new LinkedUsageInfo( LinkedUsageKind.Entry, packageId, - location.Source, - location.Line, - location.Column, + sourceLocation.Source, + sourceLocation.Line, + sourceLocation.Column, packageId, - invocation.Syntax.GetLocation())); + location)); } } - - if (!IsDynamicCreationMethod(target)) - { - return; - } - - if (invocation.TargetMethod.IsGenericMethod && invocation.TargetMethod.TypeArguments.Length != 0) - { - if (invocation.TargetMethod.TypeArguments.Any(ContainsTypeParameter)) - { - AddDynamicUse(invocation, methodName); - return; - } - foreach (var typeArgument in invocation.TargetMethod.TypeArguments) - { - AddType(typeArgument, invocation.Syntax.GetLocation()); - } - return; - } - - var typeArgumentOperation = invocation.Arguments.FirstOrDefault()?.Value; - var resolvedType = ResolveRuntimeTypeValue( - typeArgumentOperation, - new HashSet(SymbolEqualityComparer.Default)); - if (resolvedType is not null) - { - if (ContainsTypeParameter(resolvedType)) - { - AddDynamicUse(invocation, methodName); - return; - } - AddType(resolvedType, invocation.Syntax.GetLocation()); - return; - } - - AddDynamicUse(invocation, methodName); } - private void AddDynamicUse(IInvocationOperation invocation, string methodName) + private void CollectBudgetCandidate(LinkedCSharpUsageCandidate candidate) { - var location = GetLocationInfo(invocation.Syntax.GetLocation()); + var location = candidate.CreateLocation(); + if (candidate.Kind == LinkedCSharpUsageCandidateKind.Dynamic) + { + return; + } + + if (candidate.Kind == LinkedCSharpUsageCandidateKind.Type && + _catalog.ControlMaps.TryGetValue(candidate.Identity, out var typeControl)) + { + AddPackageRootUsage(typeControl.PackageId, location); + } + else if (candidate.Kind == LinkedCSharpUsageCandidateKind.Call && + _catalog.ControlMaps.TryGetValue( + candidate.ContainingTypeIdentity, + out var containingControl)) + { + AddPackageRootUsage(containingControl.PackageId, location); + } + + if (candidate.Kind == LinkedCSharpUsageCandidateKind.Call && + _catalog.PackageIdsByEntry.TryGetValue(candidate.Identity, out var packageIds)) + { + var sourceLocation = GetLocationInfo(location); + foreach (var packageId in packageIds) + { + _invokedPackages.Add(packageId); + AddUsage(new LinkedUsageInfo( + LinkedUsageKind.Entry, + packageId, + sourceLocation.Source, + sourceLocation.Line, + sourceLocation.Column, + packageId, + location)); + } + } + } + + private void AddPackageRootUsage(string packageId, Location location) + { + AddUsage(new LinkedUsageInfo( + LinkedUsageKind.PackageRoot, + packageId, + GetLocationInfo(location).Source, + GetLocationInfo(location).Line, + GetLocationInfo(location).Column, + packageId, + location)); + } + + private void AddDynamicUse(string identity, Location location) + { + var sourceLocation = GetLocationInfo(location); _dynamicUses.Add(new DynamicUse( - methodName, - location.Source, - location.Line, - location.Column, - invocation.Syntax.GetLocation())); - } - - private ITypeSymbol? ResolveRuntimeTypeValue( - IOperation? operation, - ISet visitedSymbols) - { - while (operation is IConversionOperation conversion) - { - operation = conversion.Operand; - } - if (operation is ITypeOfOperation typeOfOperation) - { - return typeOfOperation.TypeOperand; - } - if (operation is not ILocalReferenceOperation localReference || - !visitedSymbols.Add(localReference.Local)) - { - return null; - } - - foreach (var syntaxReference in localReference.Local.DeclaringSyntaxReferences) - { - var syntax = syntaxReference.GetSyntax(); - var semanticModel = _compilation.GetSemanticModel(syntax.SyntaxTree); - if (semanticModel.GetOperation(syntax) is IVariableDeclaratorOperation declarator && - declarator.Initializer is not null) - { - var resolved = ResolveRuntimeTypeValue( - declarator.Initializer.Value, - visitedSymbols); - if (resolved is not null) - { - return resolved; - } - } - } - return null; + identity, + sourceLocation.Source, + sourceLocation.Line, + sourceLocation.Column, + location)); } private void CollectAxamlUsage(XElement element) @@ -736,36 +740,17 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator return packages; } - private void AddType(ITypeSymbol? type, Location? location) + private void AddType(string metadataName, Location location) { - switch (type) + if (_catalog.ControlMaps.TryGetValue(metadataName, out var controlMap)) { - case null: - return; - case IArrayTypeSymbol array: - AddType(array.ElementType, location); - return; - case IPointerTypeSymbol pointer: - AddType(pointer.PointedAtType, location); - return; - case INamedTypeSymbol named: - var definition = named.OriginalDefinition; - var metadataName = GetTypeMetadataName(definition); - if (_catalog.ControlMaps.TryGetValue(metadataName, out var controlMap)) - { - var sourceLocation = GetLocationInfo(location); - AddControlUsage( - controlMap, - sourceLocation.Source, - sourceLocation.Line, - sourceLocation.Column, - location); - } - foreach (var typeArgument in named.TypeArguments) - { - AddType(typeArgument, location); - } - return; + var sourceLocation = GetLocationInfo(location); + AddControlUsage( + controlMap, + sourceLocation.Source, + sourceLocation.Line, + sourceLocation.Column, + location); } } @@ -814,6 +799,17 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator private void AddUsage(LinkedUsageInfo usage) { + if (AnalysisBudgetExceeded && usage.Kind is not ( + LinkedUsageKind.PackageRoot or LinkedUsageKind.Entry)) + { + var packageId = GetUsagePackage(usage); + if (packageId.Length != 0) + { + AddPackageRootUsage(packageId, usage.Location ?? Location.None); + } + return; + } + var key = usage.Kind is LinkedUsageKind.PackageRoot or LinkedUsageKind.UnitRoot or LinkedUsageKind.Entry ? string.Join("\u001f", usage.Kind.ToString(), usage.Identity) @@ -824,12 +820,40 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator usage.Source, usage.Line.ToString(CultureInfo.InvariantCulture), usage.Column.ToString(CultureInfo.InvariantCulture)); + if (!_usages.ContainsKey(key) && _usages.Count >= LinkedRegistrationAnalysisBudget.MaxUsages) + { + AnalysisBudgetExceeded = true; + CompactUsagesToPackageRoots(); + if (usage.Kind is not (LinkedUsageKind.PackageRoot or LinkedUsageKind.Entry)) + { + var packageId = GetUsagePackage(usage); + if (packageId.Length != 0) + { + AddPackageRootUsage(packageId, usage.Location ?? Location.None); + } + return; + } + } if (!_usages.ContainsKey(key)) { _usages.Add(key, usage); } } + private void CompactUsagesToPackageRoots() + { + var compacted = _usages.Values + .Where(usage => usage.Kind is LinkedUsageKind.PackageRoot or LinkedUsageKind.Entry) + .ToArray(); + _usages.Clear(); + foreach (var usage in compacted) + { + _usages.Add( + usage.Kind + "\u001f" + usage.Identity, + usage); + } + } + private void AddAxamlPackage(string source, string packageId) { if (!_axamlPackagesBySource.TryGetValue(source, out var packages)) @@ -896,24 +920,17 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator detail)); } - private Location? GetSourceLocation(ISymbol symbol) - { - return symbol.Locations.Where(static location => location.IsInSource) - .OrderBy(location => NormalizeSourcePath( - location.GetLineSpan().Path, - _projectDirectory), StringComparer.Ordinal) - .ThenBy(static location => location.SourceSpan.Start) - .ThenBy(static location => location.SourceSpan.Length) - .FirstOrDefault(); - } - private SourceLocation GetLocationInfo(Location? location) { - if (location is null || !location.IsInSource) + if (location is null) { return new SourceLocation("", 0, 0); } var span = location.GetLineSpan(); + if (span.Path.Length == 0) + { + return new SourceLocation("", 0, 0); + } return new SourceLocation( NormalizeSourcePath(span.Path, _projectDirectory), span.StartLinePosition.Line + 1, @@ -921,30 +938,6 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator } } - private static bool IsDynamicCreationMethod(IMethodSymbol method) - { - var containingType = GetTypeMetadataName(method.ContainingType.OriginalDefinition); - return (containingType == "System.Activator" && method.Name == "CreateInstance") || - (containingType == "System.Runtime.CompilerServices.RuntimeHelpers" && - method.Name == "GetUninitializedObject") || - (containingType == "System.Runtime.Serialization.FormatterServices" && - method.Name == "GetUninitializedObject"); - } - - private static bool ContainsTypeParameter(ITypeSymbol type) - { - return type switch - { - ITypeParameterSymbol => true, - IArrayTypeSymbol array => ContainsTypeParameter(array.ElementType), - IPointerTypeSymbol pointer => ContainsTypeParameter(pointer.PointedAtType), - INamedTypeSymbol named => named.TypeArguments.Any(ContainsTypeParameter) || - (named.ContainingType is not null && - ContainsTypeParameter(named.ContainingType)), - _ => false - }; - } - private static bool IsControlType(INamedTypeSymbol type) { for (var current = type; current is not null; current = current.BaseType) diff --git a/src/AtomUI.Generator/LinkedRegistration/Manifest/LinkedRegistrationManifestCodec.cs b/src/AtomUI.Generator/LinkedRegistration/Manifest/LinkedRegistrationManifestCodec.cs index a9622d92b..40a0744e6 100644 --- a/src/AtomUI.Generator/LinkedRegistration/Manifest/LinkedRegistrationManifestCodec.cs +++ b/src/AtomUI.Generator/LinkedRegistration/Manifest/LinkedRegistrationManifestCodec.cs @@ -19,6 +19,7 @@ internal static class LinkedRegistrationManifestCodec LinkedRegistrationProtocol.PackageManifestKey, package.PackageId, package.AssemblyName, + package.Granularity, package.EntryMethodMetadataNames, package.FullFragmentType, package.FullFragmentMethod, @@ -29,12 +30,23 @@ internal static class LinkedRegistrationManifestCodec unit.PackageId, unit.UnitId, unit.FragmentType, - unit.FragmentMethod), + unit.FragmentMethod, + unit.OrderKey.ToString(CultureInfo.InvariantCulture)), LinkedControlMapManifestRecord controlMap => Create( LinkedRegistrationProtocol.ControlMapManifestKey, controlMap.PackageId, controlMap.MetadataName, controlMap.UnitId), + LinkedUnitEdgeManifestRecord edge => Create( + LinkedRegistrationProtocol.UnitEdgeManifestKey, + edge.PackageId, + edge.SourceUnitId, + edge.TargetUnitId, + edge.EvidenceKind.ToString()), + LinkedRootUnitManifestRecord rootUnit => Create( + LinkedRegistrationProtocol.RootUnitManifestKey, + rootUnit.PackageId, + rootUnit.UnitId), LinkedUsageManifestRecord usage => Create( LinkedRegistrationProtocol.UsageManifestKey, usage.Kind.ToString(), @@ -42,6 +54,13 @@ internal static class LinkedRegistrationManifestCodec usage.Source, usage.Line.ToString(CultureInfo.InvariantCulture), usage.Column.ToString(CultureInfo.InvariantCulture)), + LinkedFallbackManifestRecord fallback => Create( + LinkedRegistrationProtocol.FallbackManifestKey, + fallback.PackageId, + fallback.Reason, + fallback.Source, + fallback.Line.ToString(CultureInfo.InvariantCulture), + fallback.Column.ToString(CultureInfo.InvariantCulture)), _ => throw new ArgumentOutOfRangeException(nameof(record), record.GetType().FullName) }; } @@ -95,7 +114,10 @@ internal static class LinkedRegistrationManifestCodec LinkedRegistrationProtocol.PackageManifestKey => DecodePackage(key, decoded), LinkedRegistrationProtocol.UnitManifestKey => DecodeUnit(key, decoded), LinkedRegistrationProtocol.ControlMapManifestKey => DecodeControlMap(key, decoded), + LinkedRegistrationProtocol.UnitEdgeManifestKey => DecodeUnitEdge(key, decoded), + LinkedRegistrationProtocol.RootUnitManifestKey => DecodeRootUnit(key, decoded), LinkedRegistrationProtocol.UsageManifestKey => DecodeUsage(key, decoded), + LinkedRegistrationProtocol.FallbackManifestKey => DecodeFallback(key, decoded), _ => throw new FormatException( $"Manifest key '{key}' is not supported by protocol version {LinkedRegistrationProtocol.ProtocolMajorVersion}.") }; @@ -124,10 +146,36 @@ internal static class LinkedRegistrationManifestCodec private static LinkedPackageManifestRecord DecodePackage(string key, string[] fields) { - RequireFieldCount(key, fields, 7); - RequireNonEmptyFields(key, fields, 0, 1, 3, 4); - var packageSharedType = EmptyToNull(fields[5]); - var packageSharedMethod = EmptyToNull(fields[6]); + if (fields.Length == 7) + { + RequireNonEmptyFields(key, fields, 0, 1, 3, 4); + var legacySharedType = EmptyToNull(fields[5]); + var legacySharedMethod = EmptyToNull(fields[6]); + if ((legacySharedType is null) != (legacySharedMethod is null)) + { + throw new FormatException( + $"Manifest '{key}' must provide both PackageShared fragment type and method, or neither."); + } + return new LinkedPackageManifestRecord( + fields[0], + fields[1], + string.Empty, + fields[2], + fields[3], + fields[4], + legacySharedType, + legacySharedMethod); + } + RequireFieldCount(key, fields, 8); + RequireNonEmptyFields(key, fields, 0, 1, 2, 4, 5); + if (!string.Equals(fields[2], "Package", StringComparison.Ordinal) && + !string.Equals(fields[2], "Directory", StringComparison.Ordinal)) + { + throw new FormatException( + $"Manifest '{key}' contains invalid registration granularity '{fields[2]}'."); + } + var packageSharedType = EmptyToNull(fields[6]); + var packageSharedMethod = EmptyToNull(fields[7]); if ((packageSharedType is null) != (packageSharedMethod is null)) { throw new FormatException( @@ -140,15 +188,26 @@ internal static class LinkedRegistrationManifestCodec fields[2], fields[3], fields[4], + fields[5], packageSharedType, packageSharedMethod); } private static LinkedUnitManifestRecord DecodeUnit(string key, string[] fields) { - RequireFieldCount(key, fields, 4); + if (fields.Length == 4) + { + RequireNonEmptyFields(key, fields, 0, 1, 2, 3); + return new LinkedUnitManifestRecord(fields[0], fields[1], fields[2], fields[3], 0); + } + RequireFieldCount(key, fields, 5); RequireNonEmptyFields(key, fields, 0, 1, 2, 3); - return new LinkedUnitManifestRecord(fields[0], fields[1], fields[2], fields[3]); + if (!int.TryParse(fields[4], NumberStyles.None, CultureInfo.InvariantCulture, out var orderKey) || + orderKey < 0) + { + throw new FormatException($"Manifest '{key}' contains an invalid Unit order key."); + } + return new LinkedUnitManifestRecord(fields[0], fields[1], fields[2], fields[3], orderKey); } private static LinkedControlMapManifestRecord DecodeControlMap(string key, string[] fields) @@ -158,6 +217,25 @@ internal static class LinkedRegistrationManifestCodec return new LinkedControlMapManifestRecord(fields[0], fields[1], fields[2]); } + private static LinkedUnitEdgeManifestRecord DecodeUnitEdge(string key, string[] fields) + { + RequireFieldCount(key, fields, 4); + RequireNonEmptyFields(key, fields, 0, 1, 2, 3); + if (!Enum.TryParse(fields[3], ignoreCase: false, out LinkedUnitEdgeEvidenceKind evidenceKind)) + { + throw new FormatException( + $"Manifest '{key}' contains unknown UnitEdge evidence kind '{fields[3]}'."); + } + return new LinkedUnitEdgeManifestRecord(fields[0], fields[1], fields[2], evidenceKind); + } + + private static LinkedRootUnitManifestRecord DecodeRootUnit(string key, string[] fields) + { + RequireFieldCount(key, fields, 2); + RequireNonEmptyFields(key, fields, 0, 1); + return new LinkedRootUnitManifestRecord(fields[0], fields[1]); + } + private static LinkedUsageManifestRecord DecodeUsage(string key, string[] fields) { RequireFieldCount(key, fields, 5); @@ -174,6 +252,18 @@ internal static class LinkedRegistrationManifestCodec return new LinkedUsageManifestRecord(kind, fields[1], fields[2], line, column); } + private static LinkedFallbackManifestRecord DecodeFallback(string key, string[] fields) + { + RequireFieldCount(key, fields, 5); + RequireNonEmptyFields(key, fields, 1, 2); + if (!int.TryParse(fields[3], NumberStyles.None, CultureInfo.InvariantCulture, out var line) || line < 0 || + !int.TryParse(fields[4], NumberStyles.None, CultureInfo.InvariantCulture, out var column) || column < 0) + { + throw new FormatException($"Manifest '{key}' contains an invalid source location."); + } + return new LinkedFallbackManifestRecord(fields[0], fields[1], fields[2], line, column); + } + private static void RequireFieldCount(string key, string[] fields, int expected) { if (fields.Length != expected) diff --git a/src/AtomUI.Generator/LinkedRegistration/Manifest/LinkedRegistrationManifestModels.cs b/src/AtomUI.Generator/LinkedRegistration/Manifest/LinkedRegistrationManifestModels.cs index 57b4f26ed..490d543c9 100644 --- a/src/AtomUI.Generator/LinkedRegistration/Manifest/LinkedRegistrationManifestModels.cs +++ b/src/AtomUI.Generator/LinkedRegistration/Manifest/LinkedRegistrationManifestModels.cs @@ -12,9 +12,31 @@ internal sealed record LinkedPackageManifestRecord : LinkedRegistrationManifestR string fullFragmentMethod, string? packageSharedFragmentType, string? packageSharedFragmentMethod) + : this( + packageId, + assemblyName, + "Package", + entryMethodMetadataNames, + fullFragmentType, + fullFragmentMethod, + packageSharedFragmentType, + packageSharedFragmentMethod) + { + } + + internal LinkedPackageManifestRecord( + string packageId, + string assemblyName, + string granularity, + string entryMethodMetadataNames, + string fullFragmentType, + string fullFragmentMethod, + string? packageSharedFragmentType, + string? packageSharedFragmentMethod) { PackageId = packageId; AssemblyName = assemblyName; + Granularity = granularity; EntryMethodMetadataNames = entryMethodMetadataNames; FullFragmentType = fullFragmentType; FullFragmentMethod = fullFragmentMethod; @@ -24,6 +46,7 @@ internal sealed record LinkedPackageManifestRecord : LinkedRegistrationManifestR internal string PackageId { get; } internal string AssemblyName { get; } + internal string Granularity { get; } internal string EntryMethodMetadataNames { get; } internal string FullFragmentType { get; } internal string FullFragmentMethod { get; } @@ -38,17 +61,29 @@ internal sealed record LinkedUnitManifestRecord : LinkedRegistrationManifestReco string unitId, string fragmentType, string fragmentMethod) + : this(packageId, unitId, fragmentType, fragmentMethod, 0) + { + } + + internal LinkedUnitManifestRecord( + string packageId, + string unitId, + string fragmentType, + string fragmentMethod, + int orderKey) { PackageId = packageId; UnitId = unitId; FragmentType = fragmentType; FragmentMethod = fragmentMethod; + OrderKey = orderKey; } internal string PackageId { get; } internal string UnitId { get; } internal string FragmentType { get; } internal string FragmentMethod { get; } + internal int OrderKey { get; } } internal sealed record LinkedControlMapManifestRecord : LinkedRegistrationManifestRecord @@ -68,6 +103,46 @@ internal sealed record LinkedControlMapManifestRecord : LinkedRegistrationManife internal string UnitId { get; } } +internal enum LinkedUnitEdgeEvidenceKind +{ + CSharpType, + CSharpCall, + AxamlType, + PackageCore +} + +internal sealed record LinkedUnitEdgeManifestRecord : LinkedRegistrationManifestRecord +{ + internal LinkedUnitEdgeManifestRecord( + string packageId, + string sourceUnitId, + string targetUnitId, + LinkedUnitEdgeEvidenceKind evidenceKind) + { + PackageId = packageId; + SourceUnitId = sourceUnitId; + TargetUnitId = targetUnitId; + EvidenceKind = evidenceKind; + } + + internal string PackageId { get; } + internal string SourceUnitId { get; } + internal string TargetUnitId { get; } + internal LinkedUnitEdgeEvidenceKind EvidenceKind { get; } +} + +internal sealed record LinkedRootUnitManifestRecord : LinkedRegistrationManifestRecord +{ + internal LinkedRootUnitManifestRecord(string packageId, string unitId) + { + PackageId = packageId; + UnitId = unitId; + } + + internal string PackageId { get; } + internal string UnitId { get; } +} + internal enum LinkedUsageKind { Control, @@ -99,6 +174,29 @@ internal sealed record LinkedUsageManifestRecord : LinkedRegistrationManifestRec internal int Column { get; } } +internal sealed record LinkedFallbackManifestRecord : LinkedRegistrationManifestRecord +{ + internal LinkedFallbackManifestRecord( + string packageId, + string reason, + string source, + int line, + int column) + { + PackageId = packageId; + Reason = reason; + Source = source; + Line = line; + Column = column; + } + + internal string PackageId { get; } + internal string Reason { get; } + internal string Source { get; } + internal int Line { get; } + internal int Column { get; } +} + internal sealed record LinkedRegistrationManifestEnvelope { internal LinkedRegistrationManifestEnvelope(string key, string value) diff --git a/src/AtomUI.Generator/Registration/ControlPackageRegistrationWriter.cs b/src/AtomUI.Generator/Registration/ControlPackageRegistrationWriter.cs index 50e6f71ec..05afa8702 100644 --- a/src/AtomUI.Generator/Registration/ControlPackageRegistrationWriter.cs +++ b/src/AtomUI.Generator/Registration/ControlPackageRegistrationWriter.cs @@ -10,18 +10,21 @@ internal sealed class ControlPackageRegistrationWriter private readonly string _generatedNamespace; private readonly string _assemblyName; private readonly string _packageId; + private readonly LinkedRegistration.Model.RegistrationUnitGranularity _granularity; private readonly string _entryMethodMetadataNames; internal ControlPackageRegistrationWriter( SourceProductionContext context, string? assemblyName, string packageId, + LinkedRegistration.Model.RegistrationUnitGranularity granularity, string entryMethodMetadataNames) { _context = context; _assemblyName = assemblyName ?? "AtomUI"; _generatedNamespace = GeneratedCodeNamespace.ForAssembly(assemblyName); _packageId = packageId; + _granularity = granularity; _entryMethodMetadataNames = entryMethodMetadataNames; } @@ -106,6 +109,7 @@ internal sealed class ControlPackageRegistrationWriter new LinkedRegistration.Manifest.LinkedPackageManifestRecord( _packageId, _assemblyName, + _granularity.ToString(), _entryMethodMetadataNames, _generatedNamespace + ".LinkedRegistrationV1.GeneratedFullControlPackageRegistrationFragment", "Register", diff --git a/src/AtomUI.Generator/ThemeAssets/ThemeAssetManifestWriter.cs b/src/AtomUI.Generator/ThemeAssets/ThemeAssetManifestWriter.cs index 866ff3fb4..aa2b8378c 100644 --- a/src/AtomUI.Generator/ThemeAssets/ThemeAssetManifestWriter.cs +++ b/src/AtomUI.Generator/ThemeAssets/ThemeAssetManifestWriter.cs @@ -84,13 +84,35 @@ internal sealed class ThemeAssetManifestWriter { LinkedRegistration.Manifest.LinkedRegistrationMetadataWriter.Write( builder, - new LinkedRegistration.Manifest.LinkedUsageManifestRecord( - LinkedRegistration.Manifest.LinkedUsageKind.PackageRoot, + new LinkedRegistration.Manifest.LinkedFallbackManifestRecord( _packageId, + "UnresolvedAxaml", asset.AssetPath, 0, 0)); } + foreach (var edge in _assets.SelectMany(static asset => asset.ReferencedUnitIds.Select( + dependencyUnitId => new + { + SourceUnitId = asset.OwnerUnitId, + TargetUnitId = dependencyUnitId + })) + .Where(static edge => !string.Equals( + edge.SourceUnitId, + edge.TargetUnitId, + StringComparison.Ordinal)) + .Distinct() + .OrderBy(static edge => edge.SourceUnitId, StringComparer.Ordinal) + .ThenBy(static edge => edge.TargetUnitId, StringComparer.Ordinal)) + { + LinkedRegistration.Manifest.LinkedRegistrationMetadataWriter.Write( + builder, + new LinkedRegistration.Manifest.LinkedUnitEdgeManifestRecord( + _packageId, + edge.SourceUnitId, + edge.TargetUnitId, + LinkedRegistration.Manifest.LinkedUnitEdgeEvidenceKind.AxamlType)); + } builder.AppendLine(); builder.Append("namespace ").Append(GeneratedCodeNamespace.ForAssembly(_assemblyName)) .AppendLine(".LinkedRegistrationV1;"); @@ -134,19 +156,6 @@ internal sealed class ThemeAssetManifestWriter var fragmentName = LinkedRegistration.LinkedRegistrationFragmentName.ForUnit(unitId); builder.Append("public static partial class ").Append(fragmentName).AppendLine(); builder.AppendLine("{"); - builder.AppendLine(" static partial void AddDependencies("); - builder.AppendLine(" global::AtomUI.Registration.AotTrimControlPackageRegistrationBuilder builder)"); - builder.AppendLine(" {"); - foreach (var dependencyUnitId in unitAssets.SelectMany(static asset => asset.ReferencedUnitIds) - .Distinct(StringComparer.Ordinal) - .OrderBy(static dependency => dependency, StringComparer.Ordinal)) - { - builder.Append(" ") - .Append(LinkedRegistration.LinkedRegistrationFragmentName.ForUnit(dependencyUnitId)) - .AppendLine(".Add(builder);"); - } - builder.AppendLine(" }"); - builder.AppendLine(); builder.AppendLine(" static partial void AddThemes("); builder.AppendLine(" global::AtomUI.Registration.AotTrimControlPackageRegistrationBuilder builder)"); builder.AppendLine(" {"); diff --git a/src/AtomUI.Generator/ThemeSchema/GeneratedThemeSchemaWriter.cs b/src/AtomUI.Generator/ThemeSchema/GeneratedThemeSchemaWriter.cs index 6fc57359a..900a9bbc7 100644 --- a/src/AtomUI.Generator/ThemeSchema/GeneratedThemeSchemaWriter.cs +++ b/src/AtomUI.Generator/ThemeSchema/GeneratedThemeSchemaWriter.cs @@ -15,8 +15,6 @@ internal sealed class GeneratedThemeSchemaWriter private readonly IReadOnlyList _globalTokens; private readonly IReadOnlyList _controls; private readonly IReadOnlyList _algorithms; - private readonly LinkedRegistration.Model.RegistrationUnitDependencyAnalysis - _dependencyAnalysis; internal GeneratedThemeSchemaWriter( SourceProductionContext context, @@ -26,8 +24,7 @@ internal sealed class GeneratedThemeSchemaWriter string controlCatalog, IEnumerable globalTokens, IEnumerable controls, - IEnumerable algorithms, - LinkedRegistration.Model.RegistrationUnitDependencyAnalysis dependencyAnalysis) + IEnumerable algorithms) { _context = context; _generatedNamespace = GeneratedCodeNamespace.ForAssembly(assemblyName); @@ -40,7 +37,6 @@ internal sealed class GeneratedThemeSchemaWriter _algorithms = algorithms.OrderBy(static algorithm => algorithm.AlgorithmValue) .ThenBy(static algorithm => algorithm.TypeName, StringComparer.Ordinal) .ToArray(); - _dependencyAnalysis = dependencyAnalysis; } internal void Write() @@ -117,8 +113,9 @@ internal sealed class GeneratedThemeSchemaWriter var units = _controls.GroupBy(static control => control.UnitId, StringComparer.Ordinal) .OrderBy(static group => group.Key, StringComparer.Ordinal) .ToArray(); - foreach (var unit in units) + for (var unitIndex = 0; unitIndex < units.Length; unitIndex++) { + var unit = units[unitIndex]; var fragmentName = LinkedRegistration.LinkedRegistrationFragmentName.ForUnit(unit.Key); LinkedRegistration.Manifest.LinkedRegistrationMetadataWriter.Write( source, @@ -126,7 +123,8 @@ internal sealed class GeneratedThemeSchemaWriter _packageId, unit.Key, _generatedNamespace + ".LinkedRegistrationV1." + fragmentName, - "Add")); + "Add", + unitIndex)); foreach (var control in unit.Where(static control => control.OwnsControlMap) .OrderBy(static control => control.ControlMetadataName, @@ -140,17 +138,6 @@ internal sealed class GeneratedThemeSchemaWriter unit.Key)); } } - foreach (var issue in _dependencyAnalysis.Issues) - { - LinkedRegistration.Manifest.LinkedRegistrationMetadataWriter.Write( - source, - new LinkedRegistration.Manifest.LinkedUsageManifestRecord( - LinkedRegistration.Manifest.LinkedUsageKind.PackageRoot, - _packageId, - issue.Source, - issue.Line, - issue.Column)); - } source.AppendLine(); source.Append("namespace ").Append(_generatedNamespace).AppendLine(".LinkedRegistrationV1;"); source.AppendLine(); @@ -164,21 +151,6 @@ internal sealed class GeneratedThemeSchemaWriter source.AppendLine(" global::AtomUI.Registration.AotTrimControlPackageRegistrationBuilder builder)"); source.AppendLine(" {"); source.AppendLine(" global::System.ArgumentNullException.ThrowIfNull(builder);"); - source.Append(" if (!builder.TryEnterUnit(") - .Append(SymbolDisplay.FormatLiteral(unit.Key, quote: true)) - .AppendLine("))"); - source.AppendLine(" {"); - source.AppendLine(" return;"); - source.AppendLine(" }"); - source.AppendLine(); - foreach (var dependencyUnitId in _dependencyAnalysis.GetDependencies(unit.Key)) - { - source.Append(" ") - .Append(LinkedRegistration.LinkedRegistrationFragmentName.ForUnit( - dependencyUnitId)) - .AppendLine(".Add(builder);"); - } - source.AppendLine(" AddDependencies(builder);"); foreach (var control in unit.Where(static control => control.HasDescriptor) .OrderBy(static control => control.ControlMetadataName, @@ -193,9 +165,6 @@ internal sealed class GeneratedThemeSchemaWriter source.AppendLine(" AddThemes(builder);"); source.AppendLine(" }"); source.AppendLine(); - source.AppendLine(" static partial void AddDependencies("); - source.AppendLine(" global::AtomUI.Registration.AotTrimControlPackageRegistrationBuilder builder);"); - source.AppendLine(); source.AppendLine(" static partial void AddThemes("); source.AppendLine(" global::AtomUI.Registration.AotTrimControlPackageRegistrationBuilder builder);"); source.AppendLine("}"); diff --git a/src/AtomUI.Generator/TokenResourceKeyGenerator.cs b/src/AtomUI.Generator/TokenResourceKeyGenerator.cs index 1dba4f4dd..892c4af8a 100644 --- a/src/AtomUI.Generator/TokenResourceKeyGenerator.cs +++ b/src/AtomUI.Generator/TokenResourceKeyGenerator.cs @@ -132,35 +132,6 @@ public class TokenResourceKeyGenerator : IIncrementalGenerator generationInfo.Right.OptionsProvider, context.ReportDiagnostic); tokenInfo.ControlThemeInfos.AddRange(controlThemeInfos); - var dependencyAnalysis = generationInfo.Right.EntryMethods.HasEntries - ? LinkedRegistration.Model.RegistrationUnitDependencyAnalyzer.Analyze( - generationInfo.Right.Compilation, - controlThemeInfos, - generationInfo.Right.PackageId, - generationInfo.Right.RegistrationGranularity, - generationInfo.Right.ProjectDirectory, - generationInfo.Right.OptionsProvider, - context.CancellationToken) - : LinkedRegistration.Model.RegistrationUnitDependencyAnalysis.Empty; - if (LinkedRegistration.LinkedRegistrationOptions.IsLinkedPublish( - generationInfo.Right.OptionsProvider) || - LinkedRegistration.LinkedRegistrationOptions.IsRegistrationStrict( - generationInfo.Right.OptionsProvider)) - { - var descriptor = LinkedRegistration.LinkedRegistrationOptions.IsRegistrationStrict( - generationInfo.Right.OptionsProvider) - ? AsError(Diagnostics.AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened) - : Diagnostics.AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened; - foreach (var issue in dependencyAnalysis.Issues) - { - context.ReportDiagnostic(Diagnostic.Create( - descriptor, - issue.Location ?? Location.None, - issue.Identity, - generationInfo.Right.PackageId)); - } - } - if (tokenInfo.SchemaTokens.Count != 0 || tokenInfo.ControlThemeInfos.Count != 0 || combinedInfos.Left.Right.Length != 0) @@ -173,8 +144,7 @@ public class TokenResourceKeyGenerator : IIncrementalGenerator generationInfo.Right.ControlCatalog, tokenInfo.SchemaTokens, tokenInfo.ControlThemeInfos, - combinedInfos.Left.Right, - dependencyAnalysis); + combinedInfos.Left.Right); schemaWriter.Write(); } @@ -184,6 +154,7 @@ public class TokenResourceKeyGenerator : IIncrementalGenerator context, generationInfo.Right.AssemblyName, generationInfo.Right.PackageId, + generationInfo.Right.RegistrationGranularity, generationInfo.Right.EntryMethods.ManifestMethodMetadataNames).Write(); } @@ -198,20 +169,6 @@ public class TokenResourceKeyGenerator : IIncrementalGenerator }); } - private static DiagnosticDescriptor AsError(DiagnosticDescriptor descriptor) - { - return new DiagnosticDescriptor( - descriptor.Id, - descriptor.Title, - descriptor.MessageFormat, - descriptor.Category, - DiagnosticSeverity.Error, - descriptor.IsEnabledByDefault, - descriptor.Description, - descriptor.HelpLinkUri, - descriptor.CustomTags.ToArray()); - } - private static ThemeCompilationInfo CreateCompilationInfo( Compilation compilation, Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptionsProvider optionsProvider) diff --git a/tests/AtomUI.Desktop.Controls.Tests/Theme/DesktopControlsGeneralLayoutThemeScopeMigrationTests.cs b/tests/AtomUI.Desktop.Controls.Tests/Theme/DesktopControlsGeneralLayoutThemeScopeMigrationTests.cs index f07fb27db..949df2a45 100644 --- a/tests/AtomUI.Desktop.Controls.Tests/Theme/DesktopControlsGeneralLayoutThemeScopeMigrationTests.cs +++ b/tests/AtomUI.Desktop.Controls.Tests/Theme/DesktopControlsGeneralLayoutThemeScopeMigrationTests.cs @@ -9,11 +9,11 @@ public class DesktopControlsGeneralLayoutThemeScopeMigrationTests public void General_And_Layout_Controls_Remove_Legacy_Token_Scope_Registration() { AssertNoLegacyScope( - "src/AtomUI.Desktop.Controls/Buttons/ButtonToken.cs", - "src/AtomUI.Desktop.Controls/Buttons/IconButton.cs", - "src/AtomUI.Desktop.Controls/Buttons/HyperLinkButton.cs", - "src/AtomUI.Desktop.Controls/Buttons/SplitButton.cs", - "src/AtomUI.Desktop.Controls/Buttons/ToggleIconButton.cs"); + "src/AtomUI.Desktop.Controls/Button/ButtonToken.cs", + "src/AtomUI.Desktop.Controls/Button/IconButton.cs", + "src/AtomUI.Desktop.Controls/Button/HyperLinkButton.cs", + "src/AtomUI.Desktop.Controls/SplitButton/SplitButton.cs", + "src/AtomUI.Desktop.Controls/Button/ToggleIconButton.cs"); AssertNoLegacyScope( "src/AtomUI.Desktop.Controls/FloatButton/FloatButtonToken.cs", diff --git a/tests/AtomUI.Generator.Tests/AtomUI.Generator.Tests.csproj b/tests/AtomUI.Generator.Tests/AtomUI.Generator.Tests.csproj index abce5d20b..086016fdb 100644 --- a/tests/AtomUI.Generator.Tests/AtomUI.Generator.Tests.csproj +++ b/tests/AtomUI.Generator.Tests/AtomUI.Generator.Tests.csproj @@ -16,5 +16,7 @@ + diff --git a/tests/AtomUI.Generator.Tests/BuildInfrastructure/BuildLayoutTests.cs b/tests/AtomUI.Generator.Tests/BuildInfrastructure/BuildLayoutTests.cs index c3d6b48ce..57d856957 100644 --- a/tests/AtomUI.Generator.Tests/BuildInfrastructure/BuildLayoutTests.cs +++ b/tests/AtomUI.Generator.Tests/BuildInfrastructure/BuildLayoutTests.cs @@ -13,6 +13,7 @@ public sealed class BuildLayoutTests "AtomUI.GeneratorConsumer.targets", "AtomUI.LinkedRegistration.props", "AtomUI.LinkedRegistration.targets", + "AtomUI.LinkedRegistration.SidecarConsumer.targets", "AtomUI.Localization.props", "AtomUI.Localization.targets", "AtomUI.Repository.props", @@ -31,6 +32,7 @@ public sealed class BuildLayoutTests "AtomUI.Generator.targets", "AtomUI.LinkedRegistration.props", "AtomUI.LinkedRegistration.targets", + "AtomUI.LinkedRegistration.SidecarConsumer.targets", "AtomUI.Localization.props", "AtomUI.Localization.targets", "AtomUI.ThemeAssets.targets" @@ -95,6 +97,10 @@ public sealed class BuildLayoutTests .Value.ShouldBe("tools/netstandard2.0/%(Filename)%(Extension)"); var generatorProject = XDocument.Load(GetRepoFile("src/AtomUI.Generator/AtomUI.Generator.csproj")); + generatorProject.Descendants("ProjectReference") + .ShouldNotContain(element => + ((string?)element.Attribute("Include") ?? string.Empty) + .Contains("AtomUI.Generator.LinkedPublish", StringComparison.Ordinal)); generatorProject.Descendants("None") .ShouldContain(element => (string?)element.Attribute("Include") == "@(AtomUINuGetBuildAsset)"); @@ -109,6 +115,22 @@ public sealed class BuildLayoutTests repositoryTargets.Descendants("None") .ShouldContain(element => (string?)element.Attribute("Include") == "@(AtomUIGeneratorToolAsset)"); + + var linkedPackTarget = generatorProject.Descendants("Target") + .Single(element => + (string?)element.Attribute("Name") == + "AtomUIPrepareLinkedPublishGeneratorForPack"); + ((string?)linkedPackTarget.Attribute("BeforeTargets")).ShouldBe("_GetPackageFiles"); + linkedPackTarget.Descendants("MSBuild") + .Single() + .Attribute("Projects")! + .Value.ShouldContain("AtomUI.Generator.LinkedPublish.csproj"); + ((string?)linkedPackTarget.Descendants("MSBuild").Single().Attribute("Condition")) + .ShouldBe("'$(NoBuild)' != 'true'"); + linkedPackTarget.Descendants("Error") + .Single() + .Attribute("Condition")! + .Value.ShouldContain("AtomUILinkedPublishGeneratorAssembly"); } [Fact] @@ -230,6 +252,18 @@ public sealed class BuildLayoutTests } } + [Fact] + public void Repository_Default_Items_Always_Exclude_Stale_Generated_Files() + { + var projectDefaults = XDocument.Load(GetRepoFile("build/ProjectDefaults.props")); + var excludes = projectDefaults.Descendants("DefaultItemExcludes") + .ShouldHaveSingleItem() + .Value; + + excludes.ShouldContain("$(MSBuildProjectDirectory)/obj/**"); + excludes.ShouldContain("$(MSBuildProjectDirectory)/GeneratedFiles/**"); + } + [Fact] public void Desktop_Project_Does_Not_Keep_Stale_Window_Directory_Exclusions() { diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/ApplicationRegistrationPlanGeneratorTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/ApplicationRegistrationPlanGeneratorTests.cs index aa2161003..ace8fa6e2 100644 --- a/tests/AtomUI.Generator.Tests/LinkedRegistration/ApplicationRegistrationPlanGeneratorTests.cs +++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/ApplicationRegistrationPlanGeneratorTests.cs @@ -1,7 +1,11 @@ +extern alias LinkedPublish; + using System.Collections.Immutable; using System.Text; using AtomUI.Generator.LinkedRegistration; using AtomUI.Generator.LinkedRegistration.Manifest; +using ApplicationRegistrationPlanGenerator = LinkedPublish::AtomUI.Generator.LinkedRegistration.ApplicationRegistrationPlanGenerator; +using LinkedRegistrationUsageGenerator = LinkedPublish::AtomUI.Generator.LinkedRegistration.LinkedRegistrationUsageGenerator; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; @@ -439,6 +443,7 @@ internal static class PlanGeneratorTestHost new LinkedPackageManifestRecord( packageId, packageId, + units.Count == 0 ? "Package" : "Directory", entry, fullFragmentType, "Register", @@ -561,19 +566,15 @@ internal static class PlanGeneratorTestHost ["build_property.AtomUILinkedPublish"] = "true", ["build_property.AtomUIRegistrationPlanOwner"] = "true" }; - var generatorType = typeof(ThemeAssetManifestGenerator).Assembly.GetType( - "AtomUI.Generator.LinkedRegistration.ApplicationRegistrationPlanGenerator"); - generatorType.ShouldNotBeNull("Task 8 application plan generator has not been implemented"); - var generator = Activator.CreateInstance(generatorType!).ShouldBeAssignableTo(); - var usageGeneratorType = typeof(ThemeAssetManifestGenerator).Assembly.GetType( - "AtomUI.Generator.LinkedRegistration.LinkedRegistrationUsageGenerator"); - usageGeneratorType.ShouldNotBeNull(); - var usageGenerator = Activator.CreateInstance(usageGeneratorType!) - .ShouldBeAssignableTo(); + var sidecars = UsageGeneratorTestHost.GetSidecars(references); GeneratorDriver driver = CSharpGeneratorDriver.Create( - [usageGenerator!.AsSourceGenerator(), generator!.AsSourceGenerator()], + [ + new LinkedRegistrationUsageGenerator().AsSourceGenerator(), + new ApplicationRegistrationPlanGenerator().AsSourceGenerator() + ], + sidecars.Cast().ToImmutableArray(), parseOptions: parseOptions, - optionsProvider: new PlanOptionsProvider(effectiveOptions)); + optionsProvider: new PlanOptionsProvider(effectiveOptions, sidecars)); driver = driver.RunGeneratorsAndUpdateCompilation( compilation, out var outputCompilation, @@ -592,13 +593,26 @@ internal static class PlanGeneratorTestHost .ToArray()); } - private sealed class PlanOptionsProvider(IReadOnlyDictionary values) - : AnalyzerConfigOptionsProvider + private sealed class PlanOptionsProvider : AnalyzerConfigOptionsProvider { - private readonly AnalyzerConfigOptions _options = new PlanOptions(values); + private readonly AnalyzerConfigOptions _options; + private readonly IReadOnlyDictionary _fileOptions; + + internal PlanOptionsProvider( + IReadOnlyDictionary values, + IReadOnlyList files) + { + _options = new PlanOptions(values); + _fileOptions = files.ToDictionary( + static file => file.Path, + static file => (AnalyzerConfigOptions)new PlanOptions(file.Metadata), + StringComparer.Ordinal); + } + public override AnalyzerConfigOptions GlobalOptions => _options; public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => _options; - public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => _options; + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => + _fileOptions.TryGetValue(textFile.Path, out var options) ? options : _options; } private sealed class PlanOptions(IReadOnlyDictionary values) : AnalyzerConfigOptions diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationBuildAssetsTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationBuildAssetsTests.cs index c4e27047e..e784dbe48 100644 --- a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationBuildAssetsTests.cs +++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationBuildAssetsTests.cs @@ -10,7 +10,6 @@ public sealed class LinkedRegistrationBuildAssetsTests [InlineData("PublishTrimmed")] [InlineData("PublishAot")] [InlineData("RunAOTCompilation")] - [InlineData("AtomUIUseGeneratedRegistration")] public void Linked_Mode_Inputs_Are_Normalized(string propertyName) { var targets = XDocument.Load(GetRepoFile("build/AtomUI.LinkedRegistration.targets")); @@ -28,6 +27,17 @@ public sealed class LinkedRegistrationBuildAssetsTests }).ShouldBeTrue(); } + [Fact] + public void Generated_Registration_Does_Not_Enable_Linked_Analysis() + { + var targets = XDocument.Load(GetRepoFile("build/AtomUI.LinkedRegistration.targets")); + targets.Descendants() + .Where(element => element.Name.LocalName == "AtomUILinkedPublish") + .Select(element => (string?)element.Attribute("Condition") ?? string.Empty) + .ShouldAllBe(condition => + !condition.Contains("$(AtomUIUseGeneratedRegistration)", StringComparison.Ordinal)); + } + [Fact] public void Ordinary_Build_Defaults_To_Full_Registration() { @@ -54,6 +64,20 @@ public sealed class LinkedRegistrationBuildAssetsTests .ShouldBe("'$(AtomUIRegistrationGranularity)' == ''"); } + [Fact] + public void Sidecar_Output_Is_Derived_After_Target_Framework_Output_Path() + { + var props = XDocument.Load(GetRepoFile("build/AtomUI.LinkedRegistration.props")); + var targets = XDocument.Load(GetRepoFile("build/AtomUI.LinkedRegistration.targets")); + + props.Descendants("AtomUILinkedSidecarOutputPath").ShouldBeEmpty(); + var outputPath = targets.Descendants("AtomUILinkedSidecarOutputPath").ShouldHaveSingleItem(); + ((string?)outputPath.Attribute("Condition")) + .ShouldBe("'$(AtomUILinkedSidecarOutputPath)' == ''"); + outputPath.Value.Trim().ShouldBe( + "$(TargetPath).atomui-link.json"); + } + [Fact] public void Application_Plan_Owner_Is_Derived_After_Project_OutputType() { @@ -213,6 +237,80 @@ public sealed class LinkedRegistrationBuildAssetsTests .Value.ShouldBe("tools/netstandard2.0/%(Filename)%(Extension)"); } + [Fact] + public void Only_Registration_Product_Packages_Emit_Sidecars_When_Packing() + { + var targets = XDocument.Load(GetRepoFile("build/AtomUI.LinkedRegistration.targets")); + var target = targets.Descendants("Target") + .Single(element => + (string?)element.Attribute("Name") == + "AtomUIPrepareLinkedRegistrationSidecarForPack"); + + var condition = (string?)target.Attribute("Condition"); + condition.ShouldNotBeNull(); + condition.ShouldContain("'$(IsPackable)' == 'true'"); + condition.ShouldContain("'$(AtomUIRegistrationPackageId)' != ''"); + } + + [Fact] + public void ProjectReference_Sidecars_Are_Collected_From_Resolved_Assembly_Paths() + { + var targets = XDocument.Load(GetRepoFile("build/AtomUI.LinkedRegistration.targets")); + var collectTarget = targets.Descendants("Target") + .Single(element => + (string?)element.Attribute("Name") == + "CollectAtomUIProjectReferenceSidecars"); + var getTarget = targets.Descendants("Target") + .Single(element => + (string?)element.Attribute("Name") == + "GetAtomUILinkedRegistrationSidecar"); + + ((string?)collectTarget.Attribute("DependsOnTargets")) + .ShouldBe("ResolveReferences"); + collectTarget.Descendants("MSBuild").ShouldBeEmpty(); + collectTarget.Descendants("_AtomUILinkedReferenceSidecarCandidate") + .ShouldHaveSingleItem() + .Attribute("Include") + ?.Value.ShouldBe("@(ReferencePath->'%(FullPath).atomui-link.json')"); + + getTarget.Attribute("DependsOnTargets").ShouldBeNull(); + getTarget.Descendants("_AtomUILinkedRegistrationSidecarTargetOutput") + .ShouldAllBe(element => + (string?)element.Attribute("Include") != + "@(_AtomUIProjectReferenceSidecar)"); + + collectTarget.Descendants("RemoveDuplicates").ShouldHaveSingleItem(); + + var packTarget = targets.Descendants("Target") + .Single(element => + (string?)element.Attribute("Name") == + "AtomUIPrepareLinkedRegistrationSidecarForPack"); + ((string?)packTarget.Descendants("MSBuild").Single().Attribute("Targets")) + .ShouldBe("Build;GetAtomUILinkedRegistrationSidecar"); + } + + [Fact] + public void Sidecars_Are_Emitted_After_The_Target_Assembly_Reaches_TargetPath() + { + var targets = XDocument.Load(GetRepoFile("build/AtomUI.LinkedRegistration.targets")); + var emitTarget = targets.Descendants("Target") + .Single(element => + (string?)element.Attribute("Name") == + "EmitAtomUILinkedRegistrationSidecar"); + + var afterTargets = (string?)emitTarget.Attribute("AfterTargets"); + afterTargets.ShouldNotBeNull(); + afterTargets.ShouldBe("CopyFilesToOutputDirectory"); + afterTargets.ShouldNotContain("CoreCompile"); + var condition = (string?)emitTarget.Attribute("Condition"); + condition.ShouldNotBeNull(); + condition.ShouldContain("Exists('$(TargetPath)')"); + ((string?)emitTarget.Descendants("AtomUI.Build.Tasks.GenerateLinkedRegistrationSidecarTask") + .Single() + .Attribute("AssemblyPath")) + .ShouldBe("$(TargetPath)"); + } + [Fact] public void Embedded_Generator_Consumer_Assets_Are_Compile_Time_Only_And_Idempotent() { @@ -325,7 +423,8 @@ public sealed class LinkedRegistrationBuildAssetsTests beforeTargets.ShouldNotBeNull(); beforeTargets.ShouldContain("GenerateMSBuildEditorConfigFileShouldRun"); beforeTargets.ShouldContain("CoreCompile"); - ((string?)target.Attribute("Condition")).ShouldBeNull(); + ((string?)target.Attribute("Condition")) + .ShouldBe("'$(_AtomUICollectAxamlUsage)' == 'true'"); var inputProperty = target.Descendants() .Single(element => element.Name.LocalName == "_AtomUIHasAxamlUsageInput"); diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationPackageManifestGeneratorTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationPackageManifestGeneratorTests.cs new file mode 100644 index 000000000..3aa0910c0 --- /dev/null +++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationPackageManifestGeneratorTests.cs @@ -0,0 +1,306 @@ +extern alias LinkedPublish; + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Shouldly; +using Xunit; +using LinkedRegistrationPackageManifestGenerator = LinkedPublish::AtomUI.Generator.LinkedRegistration.LinkedRegistrationPackageManifestGenerator; + +namespace AtomUI.Generator.Tests.LinkedRegistration; + +public sealed class LinkedRegistrationPackageManifestGeneratorTests +{ + [Fact] + public void Package_Core_Attached_Property_Calls_Do_Not_Root_The_Control_Unit() + { + var result = Run( + new TestSource( + "/repo/Tooltip/ToolTip.cs", + """ + namespace Avalonia.Controls { public class Control { } } + namespace Acme.Controls + { + public sealed class ToolTip : Avalonia.Controls.Control + { + public static object? GetTip(Avalonia.Controls.Control control) => null; + public static void SetIsOpen(Avalonia.Controls.Control control, bool value) { } + } + } + """), + new TestSource( + "/repo/PackageCore/ToolTipService.cs", + """ + namespace Acme.Controls + { + internal sealed class ToolTipService + { + public object? Read(Avalonia.Controls.Control control) => ToolTip.GetTip(control); + public void Open(Avalonia.Controls.Control control) => ToolTip.SetIsOpen(control, true); + } + } + """)); + + result.Diagnostics.ShouldNotContain(diagnostic => diagnostic.Id == "ATOMUILINK002"); + result.GeneratedSource.ShouldNotContain("AtomUI.Linked.RootUnit.v1"); + result.GeneratedSource.ShouldNotContain("AtomUI.Linked.Fallback.v1"); + } + + [Theory] + [InlineData("public object Create() => new Acme.Controls.ToolTip();")] + [InlineData("public System.Type GetTypeInfo() => typeof(Acme.Controls.ToolTip);")] + public void Package_Core_Direct_Type_Evidence_Roots_The_Control_Unit(string evidence) + { + var result = Run( + new TestSource( + "/repo/Tooltip/ToolTip.cs", + """ + namespace Avalonia.Controls { public class Control { } } + namespace Acme.Controls { public class ToolTip : Avalonia.Controls.Control { } } + """), + new TestSource( + "/repo/PackageCore/Bootstrap.cs", + $$""" + namespace Acme.Controls.PackageCore + { + public class Bootstrap + { + {{evidence}} + } + } + """)); + + result.GeneratedSource.ShouldContain("AtomUI.Linked.RootUnit.v1"); + result.GeneratedSource.ShouldContain("Acme.Controls%2FTooltip"); + result.GeneratedSource.ShouldNotContain("AtomUI.Linked.Fallback.v1"); + } + + [Fact] + public void Package_Core_Control_Returning_Factory_Call_Roots_The_Result_Unit() + { + var result = Run( + new TestSource( + "/repo/Alert/Alert.cs", + """ + namespace Avalonia.Controls { public class Control { } } + namespace Acme.Controls + { + public sealed class Alert : Avalonia.Controls.Control + { + public static Alert Create() => new Alert(); + } + } + """), + new TestSource( + "/repo/PackageCore/Bootstrap.cs", + """ + namespace Acme.Controls.PackageCore + { + public sealed class Bootstrap + { + public Acme.Controls.Alert Create() => Acme.Controls.Alert.Create(); + } + } + """)); + + result.GeneratedSource.ShouldContain("AtomUI.Linked.RootUnit.v1"); + result.GeneratedSource.ShouldContain("Acme.Controls%2FAlert"); + result.GeneratedSource.ShouldNotContain("AtomUI.Linked.Fallback.v1"); + } + + [Fact] + public void Package_Core_Generic_Value_Read_Does_Not_Become_A_Control_Factory_Root() + { + var result = Run( + new TestSource( + "/repo/Primitives/Control.cs", + """ + namespace Avalonia.Controls + { + public class Control + { + public T? GetValue(object property) => default; + } + } + """), + new TestSource( + "/repo/Tooltip/ToolTip.cs", + """ + namespace Acme.Controls + { + public sealed class ToolTip : Avalonia.Controls.Control + { + public static readonly object ToolTipProperty = new object(); + } + } + """), + new TestSource( + "/repo/PackageCore/ToolTipService.cs", + """ + namespace Acme.Controls.PackageCore + { + public sealed class ToolTipService + { + public Acme.Controls.ToolTip? Read(Avalonia.Controls.Control control) => + control.GetValue(Acme.Controls.ToolTip.ToolTipProperty); + } + } + """)); + + result.GeneratedSource.ShouldNotContain("AtomUI.Linked.RootUnit.v1"); + result.GeneratedSource.ShouldNotContain("AtomUI.Linked.Fallback.v1"); + } + + [Fact] + public void Cross_Unit_Call_Still_Emits_A_Direct_Call_Edge() + { + var result = Run( + new TestSource( + "/repo/Primitives/Control.cs", + "namespace Avalonia.Controls { public class Control { } }"), + new TestSource( + "/repo/Alert/Alert.cs", + """ + namespace Acme.Controls + { + public sealed class Alert : Avalonia.Controls.Control + { + public static void Ping() { } + } + } + """), + new TestSource( + "/repo/Button/Button.cs", + """ + namespace Acme.Controls + { + public sealed class Button : Avalonia.Controls.Control + { + public void Run() => Alert.Ping(); + } + } + """)); + + result.GeneratedSource.ShouldContain("AtomUI.Linked.UnitEdge.v1"); + result.GeneratedSource.ShouldContain("Acme.Controls%2FButton"); + result.GeneratedSource.ShouldContain("Acme.Controls%2FAlert"); + result.GeneratedSource.ShouldContain("CSharpCall"); + result.GeneratedSource.ShouldNotContain("AtomUI.Linked.Fallback.v1"); + } + + [Fact] + public void Interface_Dispatch_Is_Not_A_Registration_Dependency_Or_Fallback() + { + var result = Run( + new TestSource( + "/repo/Primitives/Contracts.cs", + """ + namespace Avalonia.Controls { public class Control { } } + namespace Acme.Controls.Primitives + { + public interface IPart { void Run(); } + public sealed class PartControl : Avalonia.Controls.Control, IPart + { + public void Run() { } + } + } + """), + new TestSource( + "/repo/Button/Button.cs", + """ + namespace Acme.Controls.Buttons + { + public sealed class Button : Avalonia.Controls.Control + { + public void Run(Acme.Controls.Primitives.IPart part) => part.Run(); + } + } + """)); + + result.Diagnostics.ShouldNotContain(diagnostic => diagnostic.Id == "ATOMUILINK002"); + result.GeneratedSource.ShouldNotContain("AtomUI.Linked.Fallback.v1"); + var interfaceCallEdges = result.GeneratedSource.Split('\n').Where(static line => + line.Contains("AtomUI.Linked.UnitEdge.v1", StringComparison.Ordinal) && + line.Contains("CSharpCall", StringComparison.Ordinal)).ToArray(); + interfaceCallEdges.ShouldBeEmpty(string.Join(Environment.NewLine, interfaceCallEdges)); + } + + [Fact] + public void Cross_Unit_Delegate_Dispatch_Remains_A_Package_Fallback() + { + var result = Run( + new TestSource( + "/repo/Primitives/Contracts.cs", + """ + namespace Avalonia.Controls { public class Control { } } + namespace Acme.Controls.Primitives + { + public delegate void PartAction(); + public sealed class PartControl : Avalonia.Controls.Control { } + } + """), + new TestSource( + "/repo/Button/Button.cs", + """ + namespace Acme.Controls.Buttons + { + public sealed class Button : Avalonia.Controls.Control + { + public void Run(Acme.Controls.Primitives.PartAction action) => action(); + } + } + """)); + + result.Diagnostics.ShouldContain(diagnostic => diagnostic.Id == "ATOMUILINK002"); + result.GeneratedSource.ShouldContain("AtomUI.Linked.Fallback.v1"); + } + + private static PackageManifestGeneratorExecution Run(params TestSource[] sources) + { + var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview); + var compilation = CSharpCompilation.Create( + "Acme.Controls", + sources.Select(source => CSharpSyntaxTree.ParseText( + source.Content, + parseOptions, + source.Path)), + UsageGeneratorTestHost.PlatformReferences, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + GeneratorDriver driver = CSharpGeneratorDriver.Create( + [new LinkedRegistrationPackageManifestGenerator().AsSourceGenerator()], + parseOptions: parseOptions, + optionsProvider: new PackageOptionsProvider(new Dictionary + { + ["build_property.AtomUIRegistrationPackageId"] = "Acme.Controls", + ["build_property.AtomUIRegistrationGranularity"] = "Directory", + ["build_property.AtomUILinkedPublish"] = "true", + ["build_property.ProjectDir"] = "/repo/" + })); + driver = driver.RunGenerators(compilation, TestContext.Current.CancellationToken); + var runResult = driver.GetRunResult().Results.ShouldHaveSingleItem(); + return new PackageManifestGeneratorExecution( + runResult.GeneratedSources.ShouldHaveSingleItem().SourceText.ToString(), + runResult.Diagnostics); + } + + private sealed class PackageOptionsProvider(IReadOnlyDictionary values) + : AnalyzerConfigOptionsProvider + { + private readonly AnalyzerConfigOptions _options = new PackageOptions(values); + public override AnalyzerConfigOptions GlobalOptions => _options; + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => _options; + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => _options; + } + + private sealed class PackageOptions(IReadOnlyDictionary values) + : AnalyzerConfigOptions + { + public override bool TryGetValue(string key, out string value) => + values.TryGetValue(key, out value!); + } + + private sealed record PackageManifestGeneratorExecution( + string GeneratedSource, + ImmutableArray Diagnostics); +} diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationPublishFixturesTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationPublishFixturesTests.cs index 56e75faea..4ea2c1d84 100644 --- a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationPublishFixturesTests.cs +++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationPublishFixturesTests.cs @@ -56,10 +56,16 @@ public sealed class LinkedRegistrationPublishFixturesTests var unusedUnitSource = ReadRepoFile( "tests/AtomUI.LinkedRegistration.Fixtures/UnusedUnit/UnusedUnitFragment.cs"); + unusedUnitSource.ShouldContain("AtomUI.Linked.Package.v1"); unusedUnitSource.ShouldContain("AtomUI.Linked.Unit.v1"); - unusedUnitSource.ShouldContain("AtomUI.Desktop.Controls%2FFixtureUnused"); + unusedUnitSource.ShouldContain( + "AtomUI.LinkedRegistration.Fixtures.UnusedUnit%2FFixtureUnused"); + unusedUnitSource.ShouldNotContain("AtomUI.Desktop.Controls%2FFixtureUnused"); + unusedUnitSource.ShouldContain("public static class UnusedUnitRegistration"); + unusedUnitSource.ShouldContain("[ControlPackageRegistrationEntry]"); unusedUnitSource.ShouldContain("public static class UnusedUnitFragment"); unusedUnitSource.ShouldContain("AotTrimControlPackageRegistrationBuilder builder"); + unusedUnitSource.ShouldNotContain("TryEnterUnit"); } [Fact] @@ -120,7 +126,11 @@ public sealed class LinkedRegistrationPublishFixturesTests source.ShouldContain("local build_args=("); source.ShouldContain("build \"$project\""); source.ShouldContain("restore_fixture()"); - source.ShouldContain("dotnet restore \"$(fixture_project \"$fixture\")\""); + source.ShouldContain("local restore_args=("); + source.ShouldContain("restore \"$(fixture_project \"$fixture\")\""); + source.ShouldContain("restore_args+=(-p:AtomUILinkedPublish=true)"); + source.ShouldContain("restore_fixture \"$fixture\" false"); + source.ShouldContain("restore_fixture \"$fixture\" true"); source.ShouldContain("--no-restore"); source.ShouldContain("local fixture_assembly="); source.ShouldContain("dotnet \"$fixture_assembly\""); @@ -135,13 +145,27 @@ public sealed class LinkedRegistrationPublishFixturesTests source.ShouldContain("build/MacOSHomebrewNativeAot.targets"); source.ShouldContain("CustomAfterMicrosoftCommonTargets"); source.ShouldContain("RunAOTCompilation=true"); - source.ShouldContain("AtomUIUseGeneratedRegistration=true"); + source.ShouldContain("AtomUILinkedPublish=true"); + source.ShouldContain("AtomUIEmitVerificationPlan=true"); + source.ShouldContain("verify_minimal_plan"); + source.ShouldContain("GeneratedRegistrationUnit_Button_"); + source.ShouldContain("GeneratedRegistrationUnit_Space_"); + source.ShouldContain("for forbidden_unit in DropdownButton SplitButton Flyouts Menu TreeView Dialog Tooltip DatePicker"); + source.ShouldContain("GeneratedRegistrationUnit_${forbidden_unit}_"); + source.ShouldContain("AtomUIUseGeneratedRegistration must remain inert"); source.ShouldContain("MINIMUM_DESKTOP_REDUCTION_PERCENT=40"); source.ShouldContain("MAX_SECOND_UNIT_GROWTH_BYTES=262144"); + source.ShouldContain("MAX_MINIMAL_MAIN_BYTES_OSX_ARM64=18874368"); + source.ShouldContain("! -path '*/.dSYM/*'"); + source.ShouldContain("! -name '*.pdb'"); + source.ShouldContain("! -name '*.dbg'"); + source.ShouldContain("{ print $8 }"); source.ShouldContain("if (( reduction_percent < MINIMUM_DESKTOP_REDUCTION_PERCENT )); then"); source.ShouldContain("if (( second_unit_growth > MAX_SECOND_UNIT_GROWTH_BYTES )); then"); - source.ShouldContain("NativeAOT Minimal reduction"); - source.ShouldContain("Unused Unit NativeAOT growth"); + source.ShouldContain("minimal_main_size > MAX_MINIMAL_MAIN_BYTES_OSX_ARM64"); + source.ShouldContain("NativeAOT Minimal main executable reduction"); + source.ShouldContain("Unused Unit NativeAOT main executable growth"); + source.ShouldContain("NativeAOT Minimal main executable is"); source.ShouldNotContain("(( reduction_percent >= MINIMUM_DESKTOP_REDUCTION_PERCENT ))"); source.ShouldNotContain("(( second_unit_growth <= MAX_SECOND_UNIT_GROWTH_BYTES ))"); source.ShouldContain("MinimalWithUnusedUnit"); diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationUsageGeneratorTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationUsageGeneratorTests.cs index 6a1722d60..e97cc902b 100644 --- a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationUsageGeneratorTests.cs +++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationUsageGeneratorTests.cs @@ -1,7 +1,19 @@ +extern alias LinkedPublish; + using System.Collections.Immutable; using System.Text; using AtomUI.Generator.LinkedRegistration; using AtomUI.Generator.LinkedRegistration.Manifest; +using LinkedRegistrationSidecar = LinkedPublish::AtomUI.LinkedRegistration.Protocol.LinkedRegistrationSidecar; +using LinkedRegistrationSidecarCodec = LinkedPublish::AtomUI.LinkedRegistration.Protocol.LinkedRegistrationSidecarCodec; +using LinkedRegistrationUsageGenerator = LinkedPublish::AtomUI.Generator.LinkedRegistration.LinkedRegistrationUsageGenerator; +using LinkedSidecarAssembly = LinkedPublish::AtomUI.LinkedRegistration.Protocol.LinkedSidecarAssembly; +using LinkedSidecarFallback = LinkedPublish::AtomUI.LinkedRegistration.Protocol.LinkedSidecarFallback; +using LinkedSidecarFragment = LinkedPublish::AtomUI.LinkedRegistration.Protocol.LinkedSidecarFragment; +using LinkedSidecarPackage = LinkedPublish::AtomUI.LinkedRegistration.Protocol.LinkedSidecarPackage; +using LinkedSidecarUnit = LinkedPublish::AtomUI.LinkedRegistration.Protocol.LinkedSidecarUnit; +using LinkedSidecarUnitEdge = LinkedPublish::AtomUI.LinkedRegistration.Protocol.LinkedSidecarUnitEdge; +using LinkedSidecarUsage = LinkedPublish::AtomUI.LinkedRegistration.Protocol.LinkedSidecarUsage; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; @@ -13,6 +25,21 @@ namespace AtomUI.Generator.Tests.LinkedRegistration; public sealed class LinkedRegistrationUsageGeneratorTests { + [Fact] + public void Source_candidate_budget_emits_a_conservative_package_fallback() + { + var source = "namespace Consumer { public static class Usage { public static void Run() { " + + string.Concat(Enumerable.Repeat( + "typeof(Acme.Controls.DatePicker);", + 50_001)) + + " } }"; + + var result = UsageGeneratorTestHost.Run([source], [UsageGeneratorTestHost.AcmePackage]); + + result.GeneratedSource.ShouldContain("AtomUI.Linked.Fallback.v1"); + result.GeneratedSource.ShouldContain("AnalysisBudgetExceeded"); + } + [Theory] [InlineData("public sealed class Consumer { public T? Create() => System.Activator.CreateInstance(); }")] [InlineData("public sealed class Consumer { public object? Create() => System.Activator.CreateInstance(typeof(T)); }")] @@ -27,10 +54,10 @@ public sealed class LinkedRegistrationUsageGeneratorTests } [Fact] - public void Static_Open_Generic_Type_Position_Retains_The_Generic_Control_Unit() + public void Static_Open_Generic_Typeof_Retains_The_Generic_Control_Unit() { var result = UsageGeneratorTestHost.Run( - ["public sealed class Consumer { public Acme.Controls.GenericControl? Value { get; set; } }"] , + ["public sealed class Consumer { public System.Type Value => typeof(Acme.Controls.GenericControl<>); }"] , [UsageGeneratorTestHost.AcmePackage]); result.Usages.ShouldContain(usage => @@ -78,6 +105,18 @@ public sealed class LinkedRegistrationUsageGeneratorTests result.GeneratedSource.ShouldNotContain("AotTrimRegistrationPlan"); } + [Fact] + public void Static_Control_Member_Call_Emits_Control_Usage() + { + var result = UsageGeneratorTestHost.Run( + ["public sealed class Consumer { public void Open(Avalonia.Controls.Control control) => Acme.Controls.DatePicker.SetIsOpen(control, true); }"] , + [UsageGeneratorTestHost.AcmePackage]); + + result.Usages.ShouldContain(usage => + usage.Kind == LinkedUsageKind.Control && + usage.Identity == "Acme.Controls.DatePicker"); + } + [Fact] public void Custom_Control_Retains_The_Unit_Of_Its_AtomUI_Base() { @@ -237,7 +276,7 @@ public sealed class LinkedRegistrationUsageGeneratorTests { var library = UsageGeneratorTestHost.CompileGeneratedReference( "Consumer.Library", - ["public sealed class Consumer { private Acme.Controls.DatePicker? _picker; }"] , + ["public sealed class Consumer { public Acme.Controls.DatePicker Create() => new(); }"] , [UsageGeneratorTestHost.AcmePackage], new Dictionary { @@ -377,7 +416,7 @@ public sealed class LinkedRegistrationUsageGeneratorTests { var library = UsageGeneratorTestHost.CompileGeneratedReference( "Consumer.Library", - ["public sealed class Consumer { public Acme.Controls.DatePicker? Picker { get; set; } }"] , + ["public sealed class Consumer { public Acme.Controls.DatePicker Create() => new(); }"] , [UsageGeneratorTestHost.AcmePackage]); var result = UsageGeneratorTestHost.Run( @@ -685,7 +724,7 @@ public sealed class LinkedRegistrationUsageGeneratorTests [ new TestSource( "c:\\work\\consumer\\Controls\\PickerConsumer.cs", - "public sealed class PickerConsumer { public Acme.Controls.DatePicker? Picker { get; set; } }") + "public sealed class PickerConsumer { public Acme.Controls.DatePicker Create() => new(); }") ], [UsageGeneratorTestHost.AcmePackage], new Dictionary @@ -693,7 +732,9 @@ public sealed class LinkedRegistrationUsageGeneratorTests ["build_property.ProjectDir"] = "C:\\WORK\\CONSUMER\\" }); - result.Usages.ShouldContain(usage => usage.Source == "Controls/PickerConsumer.cs"); + result.Usages.ShouldContain( + usage => usage.Source == "Controls/PickerConsumer.cs", + string.Join(", ", result.Usages.Select(static usage => usage.Source))); } [Theory] @@ -709,7 +750,7 @@ public sealed class LinkedRegistrationUsageGeneratorTests [ new TestSource( sourcePath, - "public sealed class PickerConsumer { public Acme.Controls.DatePicker? Picker { get; set; } }") + "public sealed class PickerConsumer { public Acme.Controls.DatePicker Create() => new(); }") ], [UsageGeneratorTestHost.AcmePackage], new Dictionary @@ -717,7 +758,9 @@ public sealed class LinkedRegistrationUsageGeneratorTests ["build_property.ProjectDir"] = projectDirectory }); - result.Usages.ShouldContain(usage => usage.Source == expectedSource); + result.Usages.ShouldContain( + usage => usage.Source == expectedSource, + string.Join(", ", result.Usages.Select(static usage => usage.Source))); } [Fact] @@ -741,12 +784,9 @@ public sealed class LinkedRegistrationUsageGeneratorTests var malformed = UsageGeneratorTestHost.CreateRawManifestReference( "Malformed.Manifest", (LinkedRegistrationProtocol.ControlMapManifestKey, "1|Acme.Controls")); - var incomplete = UsageGeneratorTestHost.CreateManifestReference( + var incomplete = UsageGeneratorTestHost.CreateRawManifestReference( "Incomplete.Manifest", - new LinkedControlMapManifestRecord( - "Missing.Package", - "Missing.Controls.Picker", - "Missing.Package/Picker")); + (LinkedRegistrationProtocol.UnitManifestKey, "1|Missing.Package")); var result = UsageGeneratorTestHost.Run( ["public sealed class Consumer { }"] , @@ -903,22 +943,29 @@ public sealed class LinkedRegistrationUsageGeneratorTests } [Fact] - public void ControlMap_Must_Reference_An_Existing_Unit_Record() + public void UnitEdge_Must_Reference_Existing_Unit_Records() { var reference = UsageGeneratorTestHost.CreateManifestReference( "Missing.Unit", new LinkedPackageManifestRecord( "MissingUnit.Controls", "Missing.Unit", + "Directory", "MissingUnit.Controls.Entry.UseControls", "MissingUnit.Controls.Full", "Register", null, null), - new LinkedControlMapManifestRecord( + new LinkedUnitManifestRecord( "MissingUnit.Controls", - "MissingUnit.Controls.Picker", - "MissingUnit.Controls/Picker")); + "MissingUnit.Controls/Picker", + "MissingUnit.Controls.PickerFragment", + "Add"), + new LinkedUnitEdgeManifestRecord( + "MissingUnit.Controls", + "MissingUnit.Controls/Picker", + "MissingUnit.Controls/Missing", + LinkedUnitEdgeEvidenceKind.CSharpCall)); var result = UsageGeneratorTestHost.Run( ["public sealed class Consumer { }"] , @@ -953,11 +1000,13 @@ public sealed class LinkedRegistrationUsageGeneratorTests internal static class UsageGeneratorTestHost { - private static readonly ImmutableArray s_platformReferences = + internal static readonly ImmutableArray PlatformReferences = ((string?)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))! .Split(Path.PathSeparator) .Select(static path => (MetadataReference)MetadataReference.CreateFromFile(path)) .ToImmutableArray(); + private static readonly Dictionary s_sidecars = + new(ReferenceEqualityComparer.Instance); internal static readonly IReadOnlyDictionary LinkedEntryOptions = new Dictionary @@ -979,7 +1028,10 @@ internal static class UsageGeneratorTestHost namespace Avalonia.Controls { public class Control { } } namespace Acme.Controls { - public class DatePicker : Avalonia.Controls.Control { } + public class DatePicker : Avalonia.Controls.Control + { + public static void SetIsOpen(Avalonia.Controls.Control control, bool value) { } + } public class GenericControl : Avalonia.Controls.Control { } public class UnmappedPicker : Avalonia.Controls.Control { } public class Helper { } @@ -1038,18 +1090,16 @@ internal static class UsageGeneratorTestHost var compilation = CSharpCompilation.Create( assemblyName, syntaxTrees, - s_platformReferences.Concat(references), + PlatformReferences.Concat(references), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - var files = additionalTexts?.ToImmutableArray() ?? ImmutableArray.Empty; + var files = GetSidecars(references) + .Concat(additionalTexts ?? []) + .GroupBy(static file => file.Path, StringComparer.Ordinal) + .Select(static group => group.First()) + .ToImmutableArray(); var options = new TestOptionsProvider(files, globalOptions); - var generatorType = typeof(ThemeAssetManifestGenerator).Assembly.GetType( - "AtomUI.Generator.LinkedRegistration.LinkedRegistrationUsageGenerator"); - generatorType.ShouldNotBeNull("Task 7 usage generator has not been implemented"); - var instance = Activator.CreateInstance(generatorType!); - instance.ShouldBeAssignableTo(); - var generator = (IIncrementalGenerator)instance!; GeneratorDriver driver = CSharpGeneratorDriver.Create( - [generator.AsSourceGenerator()], + [new LinkedRegistrationUsageGenerator().AsSourceGenerator()], files.Cast().ToImmutableArray(), parseOptions, options); @@ -1080,7 +1130,9 @@ internal static class UsageGeneratorTestHost emit.Diagnostics.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ShouldBeEmpty(); emit.Success.ShouldBeTrue(); - return MetadataReference.CreateFromImage(stream.ToArray()); + var reference = MetadataReference.CreateFromImage(stream.ToArray()); + RegisterSidecar(reference, assemblyName, execution.Usages); + return reference; } internal static TestAdditionalText Axaml(string path, string content) @@ -1119,7 +1171,17 @@ internal static class UsageGeneratorTestHost { LinkedRegistrationMetadataWriter.Write(source, record); } - return CreateSourceReference(assemblyName, source.ToString(), []); + var reference = CreateSourceReference(assemblyName, source.ToString(), []); + RegisterSidecar(reference, assemblyName, records); + return reference; + } + + internal static ImmutableArray GetSidecars( + IEnumerable references) + { + return references.Where(s_sidecars.ContainsKey) + .Select(reference => s_sidecars[reference]) + .ToImmutableArray(); } internal static IReadOnlyList GetAssemblyMetadata( @@ -1127,7 +1189,7 @@ internal static class UsageGeneratorTestHost { var compilation = CSharpCompilation.Create( "MetadataReader", - references: s_platformReferences.Add(reference), + references: PlatformReferences.Add(reference), options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); var assembly = compilation.GetAssemblyOrModuleSymbol(reference).ShouldBeAssignableTo(); return GetUsageMetadata(assembly!); @@ -1177,16 +1239,20 @@ internal static class UsageGeneratorTestHost IReadOnlyList usages) { var metadata = new StringBuilder(); + var records = new List(); + var package = new LinkedPackageManifestRecord( + packageId, + packageId, + controlMaps.Count == 0 ? "Package" : "Directory", + entry, + packageId + ".Generated.Full", + "Register", + null, + null); + records.Add(package); LinkedRegistrationMetadataWriter.Write( metadata, - new LinkedPackageManifestRecord( - packageId, - packageId, - entry, - packageId + ".Generated.Full", - "Register", - null, - null)); + package); foreach (var unit in controlMaps.Select(static controlMap => new LinkedUnitManifestRecord( controlMap.PackageId, @@ -1195,18 +1261,121 @@ internal static class UsageGeneratorTestHost "Register")).Distinct()) { LinkedRegistrationMetadataWriter.Write(metadata, unit); + records.Add(unit); } foreach (var controlMap in controlMaps) { LinkedRegistrationMetadataWriter.Write(metadata, controlMap); + records.Add(controlMap); } foreach (var usage in usages) { LinkedRegistrationMetadataWriter.Write(metadata, usage); + records.Add(usage); } metadata.AppendLine(source); - return CreateSourceReference(packageId, metadata.ToString(), references); + var reference = CreateSourceReference(packageId, metadata.ToString(), references); + RegisterSidecar(reference, packageId, records); + return reference; + } + + private static void RegisterSidecar( + MetadataReference reference, + string assemblyName, + IEnumerable records) + { + var materialized = records.ToArray(); + var packageRecords = materialized.OfType().ToArray(); + var unitRecords = materialized.OfType().ToArray(); + var controlMaps = materialized.OfType().ToArray(); + var edges = materialized.OfType().ToArray(); + var roots = materialized.OfType().ToArray(); + var sidecar = new LinkedRegistrationSidecar + { + Producer = "AtomUI.Generator.Tests", + Assembly = new LinkedSidecarAssembly + { + Name = assemblyName, + TargetFramework = "net10.0" + }, + Packages = packageRecords.Select(package => new LinkedSidecarPackage + { + Id = package.PackageId, + AssemblyName = package.AssemblyName, + Granularity = package.Granularity, + EntryMethods = package.EntryMethodMetadataNames.Split( + new[] { ';' }, + StringSplitOptions.RemoveEmptyEntries) + .Select(static entry => entry.Trim()) + .Where(static entry => entry.Length != 0) + .ToArray(), + FullFragment = new LinkedSidecarFragment + { + Type = package.FullFragmentType, + Method = package.FullFragmentMethod + }, + SharedFragment = package.PackageSharedFragmentType is null + ? null + : new LinkedSidecarFragment + { + Type = package.PackageSharedFragmentType, + Method = package.PackageSharedFragmentMethod! + }, + Units = unitRecords.Where(unit => unit.PackageId == package.PackageId) + .Select(unit => new LinkedSidecarUnit + { + Id = unit.UnitId, + FragmentType = unit.FragmentType, + FragmentMethod = unit.FragmentMethod, + OrderKey = unit.OrderKey, + Controls = controlMaps.Where(control => + control.PackageId == package.PackageId && + control.UnitId == unit.UnitId) + .Select(static control => control.MetadataName) + .ToArray() + }) + .ToArray(), + UnitEdges = edges.Where(edge => edge.PackageId == package.PackageId) + .Select(static edge => new LinkedSidecarUnitEdge + { + SourceUnitId = edge.SourceUnitId, + TargetUnitId = edge.TargetUnitId, + EvidenceKind = edge.EvidenceKind.ToString() + }) + .ToArray(), + RootUnits = roots.Where(root => root.PackageId == package.PackageId) + .Select(static root => root.UnitId) + .ToArray() + }).ToArray(), + Usages = materialized.OfType() + .Select(static usage => new LinkedSidecarUsage + { + Kind = usage.Kind.ToString(), + Identity = usage.Identity, + Source = usage.Source, + Line = usage.Line, + Column = usage.Column + }) + .ToArray(), + Fallbacks = materialized.OfType() + .Select(static fallback => new LinkedSidecarFallback + { + PackageId = fallback.PackageId, + Reason = fallback.Reason, + Source = fallback.Source, + Line = fallback.Line, + Column = fallback.Column + }) + .ToArray() + }; + s_sidecars[reference] = new TestAdditionalText( + assemblyName + ".atomui-link.json", + Encoding.UTF8.GetString(LinkedRegistrationSidecarCodec.Write(sidecar)), + new Dictionary + { + ["build_metadata.AdditionalFiles.AtomUILinkedSidecar"] = "true" + }); } private static MetadataReference CreateSourceReference( @@ -1217,7 +1386,7 @@ internal static class UsageGeneratorTestHost var compilation = CSharpCompilation.Create( assemblyName, [CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview))], - s_platformReferences.Concat(references), + PlatformReferences.Concat(references), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); using var stream = new MemoryStream(); var emit = compilation.Emit(stream); diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/PackageEntryIntegrationTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/PackageEntryIntegrationTests.cs index 828007b27..d20a29c73 100644 --- a/tests/AtomUI.Generator.Tests/LinkedRegistration/PackageEntryIntegrationTests.cs +++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/PackageEntryIntegrationTests.cs @@ -92,6 +92,27 @@ public sealed class PackageEntryIntegrationTests } } + [Fact] + public void Desktop_Directory_Registration_Uses_Stable_Button_Family_Source_Boundaries() + { + var desktopProject = File.ReadAllText(GetRepoFile( + "src/AtomUI.Desktop.Controls/AtomUI.Desktop.Controls.csproj")); + + desktopProject.ShouldNotContain("AtomUIRegistrationUnit"); + File.Exists(GetRepoFile("src/AtomUI.Desktop.Controls/Button/Button.cs")).ShouldBeTrue(); + File.Exists(GetRepoFile( + "src/AtomUI.Desktop.Controls/DropdownButton/DropdownButton.cs")).ShouldBeTrue(); + File.Exists(GetRepoFile( + "src/AtomUI.Desktop.Controls/SplitButton/SplitButton.cs")).ShouldBeTrue(); + + File.Exists(GetRepoFile( + "src/AtomUI.Desktop.Controls/Buttons/Themes/ButtonTheme.axaml")).ShouldBeTrue(); + File.Exists(GetRepoFile( + "src/AtomUI.Desktop.Controls/Buttons/Themes/DropdownButtonTheme.axaml")).ShouldBeTrue(); + File.Exists(GetRepoFile( + "src/AtomUI.Desktop.Controls/Buttons/Themes/SplitButtonTheme.axaml")).ShouldBeTrue(); + } + [Theory] [InlineData("src/AtomUI.Desktop.Controls.DataGrid/AtomUI.Desktop.Controls.DataGrid.csproj")] [InlineData("src/AtomUI.Desktop.Controls.ColorPicker/AtomUI.Desktop.Controls.ColorPicker.csproj")] diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/RegistrationUnitGraphPlannerTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/RegistrationUnitGraphPlannerTests.cs new file mode 100644 index 000000000..c1cd20c2c --- /dev/null +++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/RegistrationUnitGraphPlannerTests.cs @@ -0,0 +1,63 @@ +extern alias LinkedPublish; + +using LinkedEdge = LinkedPublish::AtomUI.Generator.LinkedRegistration.Manifest.LinkedUnitEdgeManifestRecord; +using LinkedEdgeKind = LinkedPublish::AtomUI.Generator.LinkedRegistration.Manifest.LinkedUnitEdgeEvidenceKind; +using LinkedPlanner = LinkedPublish::AtomUI.Generator.LinkedRegistration.RegistrationUnitGraphPlanner; +using LinkedUnit = LinkedPublish::AtomUI.Generator.LinkedRegistration.Manifest.LinkedUnitManifestRecord; +using Shouldly; +using Xunit; + +namespace AtomUI.Generator.Tests.LinkedRegistration; + +public sealed class RegistrationUnitGraphPlannerTests +{ + [Fact] + public void Long_dependency_chain_is_planned_without_recursive_stack_growth() + { + const int unitCount = 10_000; + var units = Enumerable.Range(0, unitCount) + .Select(index => new LinkedUnit( + "Acme.Controls", + "Unit" + index, + "GeneratedUnit" + index, + "Add", + index)) + .ToArray(); + var edges = Enumerable.Range(1, unitCount - 1) + .Select(index => new LinkedEdge( + "Acme.Controls", + "Unit" + index, + "Unit" + (index - 1), + LinkedEdgeKind.CSharpCall)) + .ToArray(); + + var result = LinkedPlanner.Plan(units, edges, ["Unit" + (unitCount - 1)]); + + result.Length.ShouldBe(unitCount); + result[0].UnitId.ShouldBe("Unit0"); + result[^1].UnitId.ShouldBe("Unit9999"); + } + + [Fact] + public void Cyclic_units_are_collapsed_and_dependencies_still_precede_dependents() + { + var units = new[] + { + new LinkedUnit("Acme.Controls", "A", "GeneratedA", "Add", 10), + new LinkedUnit("Acme.Controls", "B", "GeneratedB", "Add", 20), + new LinkedUnit("Acme.Controls", "C", "GeneratedC", "Add", 30), + new LinkedUnit("Acme.Controls", "D", "GeneratedD", "Add", 40) + }; + var edges = new[] + { + new LinkedEdge("Acme.Controls", "A", "B", LinkedEdgeKind.CSharpCall), + new LinkedEdge("Acme.Controls", "B", "A", LinkedEdgeKind.CSharpCall), + new LinkedEdge("Acme.Controls", "C", "B", LinkedEdgeKind.CSharpCall), + new LinkedEdge("Acme.Controls", "D", "C", LinkedEdgeKind.CSharpCall) + }; + + var result = LinkedPlanner.Plan(units, edges, ["D"]); + + result.Select(static unit => unit.UnitId).ShouldBe(["A", "B", "C", "D"]); + } +} diff --git a/tests/AtomUI.Generator.Tests/ThemeAssetManifestGeneratorTests.cs b/tests/AtomUI.Generator.Tests/ThemeAssetManifestGeneratorTests.cs index 4ed2cc9fe..ef88993cc 100644 --- a/tests/AtomUI.Generator.Tests/ThemeAssetManifestGeneratorTests.cs +++ b/tests/AtomUI.Generator.Tests/ThemeAssetManifestGeneratorTests.cs @@ -171,7 +171,7 @@ public class ThemeAssetManifestGeneratorTests } [Fact] - public void Unit_Theme_Part_Calls_Same_Package_Unit_Dependencies_Directly() + public void Unit_Theme_Part_Emits_Same_Package_UnitEdge() { var compilation = CreateCompilation(""" namespace Demo; @@ -194,13 +194,15 @@ public class ThemeAssetManifestGeneratorTests diagnostics.ShouldBeEmpty(); var source = GetGeneratedSource(result, "GeneratedControlThemeAssetFragments.g.cs"); - var dependencyFragment = global::AtomUI.Generator.LinkedRegistration.LinkedRegistrationFragmentName.ForUnit( - "ThemeAssetManifestTests/Alert"); - source.ShouldContain($"{dependencyFragment}.Add(builder);"); + source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.UnitEdge.v1\""); + source.ShouldContain("ThemeAssetManifestTests%2FButton"); + source.ShouldContain("ThemeAssetManifestTests%2FAlert"); + source.ShouldContain("AxamlType"); + source.ShouldNotContain(".Add(builder);"); } [Fact] - public void Unit_Theme_Part_Calls_Template_Control_Unit_Dependencies_Directly() + public void Unit_Theme_Part_Emits_Template_Control_UnitEdge() { var compilation = CreateCompilation( """ @@ -249,9 +251,11 @@ public class ThemeAssetManifestGeneratorTests diagnostics.ShouldBeEmpty(); var source = GetGeneratedSource(result, "GeneratedControlThemeAssetFragments.g.cs"); - var dependencyFragment = global::AtomUI.Generator.LinkedRegistration.LinkedRegistrationFragmentName.ForUnit( - "ThemeAssetManifestTests/MarqueeLabel"); - source.ShouldContain($"{dependencyFragment}.Add(builder);"); + source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.UnitEdge.v1\""); + source.ShouldContain("ThemeAssetManifestTests%2FAlert"); + source.ShouldContain("ThemeAssetManifestTests%2FMarqueeLabel"); + source.ShouldContain("AxamlType"); + source.ShouldNotContain(".Add(builder);"); var manifest = GetGeneratedSource(result, "GeneratedControlThemeAssetManifest.g.cs"); manifest.ShouldNotContain("ControlTokenIdentity(\"AtomUI\", \"MarqueeLabel\")"); } @@ -327,8 +331,8 @@ public class ThemeAssetManifestGeneratorTests diagnostics.ShouldHaveSingleItem().Id.ShouldBe("ATOMUILINK002"); var source = GetGeneratedSource(result, "GeneratedControlThemeAssetFragments.g.cs"); - source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.Usage.v1\""); - source.ShouldContain("PackageRoot"); + source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.Fallback.v1\""); + source.ShouldContain("UnresolvedAxaml"); source.ShouldNotContain("builder.AddPackageSharedThemeAsset("); } @@ -342,8 +346,8 @@ public class ThemeAssetManifestGeneratorTests diagnostics.ShouldNotContain(diagnostic => diagnostic.Id == "ATOMUILINK002"); var source = GetGeneratedSource(result, "GeneratedControlThemeAssetFragments.g.cs"); - source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.Usage.v1\""); - source.ShouldContain("PackageRoot"); + source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.Fallback.v1\""); + source.ShouldContain("UnresolvedAxaml"); } [Fact] diff --git a/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs b/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs index 9875228e0..365fef6c0 100644 --- a/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs +++ b/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs @@ -1,9 +1,12 @@ +extern alias LinkedPublish; + using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Shouldly; using Xunit; +using LinkedRegistrationPackageManifestGenerator = LinkedPublish::AtomUI.Generator.LinkedRegistration.LinkedRegistrationPackageManifestGenerator; namespace AtomUI.Generator.Tests; @@ -170,7 +173,8 @@ public class ThemeSchemaGeneratorTests source.ShouldContain("public static partial class GeneratedRegistrationUnit_ThemeSchemaGeneratorTests_"); source.ShouldContain("public static void Add("); source.ShouldContain("global::AtomUI.Registration.AotTrimControlPackageRegistrationBuilder builder"); - source.ShouldContain("builder.TryEnterUnit(\"ThemeSchemaGeneratorTests/ThemeSchemaGeneratorTests\")"); + source.ShouldNotContain("TryEnterUnit"); + source.ShouldNotContain("AddDependencies"); source.ShouldContain("builder.AddControl("); source.ShouldContain( "global::AtomUI.Generated.ThemeSchemaGeneratorTests.GeneratedThemeSchemaDescriptorFactory.CreateControlDescriptor_Button_"); @@ -282,16 +286,17 @@ public class ThemeSchemaGeneratorTests path: "Alert/Alert.cs", cancellationToken: TestContext.Current.CancellationToken)); - var outputCompilation = RunGeneratorWithGranularity( + var outputCompilation = RunLinkedGeneratorWithGranularity( compilation, out var diagnostics, "Directory"); diagnostics.ShouldBeEmpty(); - var source = GetGeneratedSource(outputCompilation, "GeneratedRegistrationUnits.g.cs"); - var dependencyFragment = global::AtomUI.Generator.LinkedRegistration - .LinkedRegistrationFragmentName.ForUnit("ThemeSchemaGeneratorTests/Alert"); - source.ShouldContain($"{dependencyFragment}.Add(builder);"); + var source = GetGeneratedSource(outputCompilation, "LinkedRegistrationPackageAnalysis.g.cs"); + source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.UnitEdge.v1\""); + source.ShouldContain("ThemeSchemaGeneratorTests%2FButton"); + source.ShouldContain("ThemeSchemaGeneratorTests%2FAlert"); + source.ShouldContain("CSharpType"); } [Fact] @@ -325,17 +330,16 @@ public class ThemeSchemaGeneratorTests path: "Alert/Alert.cs", cancellationToken: TestContext.Current.CancellationToken)); - var outputCompilation = RunGeneratorWithGranularity( + var outputCompilation = RunLinkedGeneratorWithGranularity( compilation, out var diagnostics, "Directory"); diagnostics.ShouldBeEmpty(); - var source = GetGeneratedSource(outputCompilation, "GeneratedRegistrationUnits.g.cs"); - var dependencyFragment = global::AtomUI.Generator.LinkedRegistration - .LinkedRegistrationFragmentName.ForUnit("ThemeSchemaGeneratorTests/Alert"); - source.ShouldContain($"{dependencyFragment}.Add(builder);"); - source.ShouldNotContain("PackageRoot"); + var source = GetGeneratedSource(outputCompilation, "LinkedRegistrationPackageAnalysis.g.cs"); + source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.RootUnit.v1\""); + source.ShouldContain("ThemeSchemaGeneratorTests%2FAlert"); + source.ShouldNotContain("AtomUI.Linked.Fallback.v1"); } [Fact] @@ -371,17 +375,16 @@ public class ThemeSchemaGeneratorTests path: "Alert/Alert.cs", cancellationToken: TestContext.Current.CancellationToken)); - var outputCompilation = RunGeneratorWithGranularity( + var outputCompilation = RunLinkedGeneratorWithGranularity( compilation, out var diagnostics, "Directory"); diagnostics.ShouldBeEmpty(); - var source = GetGeneratedSource(outputCompilation, "GeneratedRegistrationUnits.g.cs"); - var dependencyFragment = global::AtomUI.Generator.LinkedRegistration - .LinkedRegistrationFragmentName.ForUnit("ThemeSchemaGeneratorTests/Alert"); - source.ShouldContain($"{dependencyFragment}.Add(builder);"); - source.ShouldNotContain("PackageRoot"); + var source = GetGeneratedSource(outputCompilation, "LinkedRegistrationPackageAnalysis.g.cs"); + source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.RootUnit.v1\""); + source.ShouldContain("ThemeSchemaGeneratorTests%2FAlert"); + source.ShouldNotContain("AtomUI.Linked.Fallback.v1"); } [Fact] @@ -485,9 +488,9 @@ public class ThemeSchemaGeneratorTests var diagnostic = diagnostics.ShouldHaveSingleItem(); diagnostic.Id.ShouldBe("ATOMUILINK002"); diagnostic.Location.GetLineSpan().Path.ShouldBe("Button/Button.cs"); - var source = GetGeneratedSource(outputCompilation, "GeneratedRegistrationUnits.g.cs"); - source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.Usage.v1\""); - source.ShouldContain("PackageRoot"); + var source = GetGeneratedSource(outputCompilation, "LinkedRegistrationPackageAnalysis.g.cs"); + source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.Fallback.v1\""); + source.ShouldContain("DynamicInvocation"); } [Fact] @@ -512,8 +515,9 @@ public class ThemeSchemaGeneratorTests var diagnostic = diagnostics.ShouldHaveSingleItem(); diagnostic.Id.ShouldBe("ATOMUILINK002"); diagnostic.Location.GetLineSpan().Path.ShouldBe("Button/Button.cs"); - var source = GetGeneratedSource(outputCompilation, "GeneratedRegistrationUnits.g.cs"); - source.ShouldContain("PackageRoot"); + var source = GetGeneratedSource(outputCompilation, "LinkedRegistrationPackageAnalysis.g.cs"); + source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.Fallback.v1\""); + source.ShouldContain("DynamicInvocation"); } [Fact] @@ -885,12 +889,21 @@ public class ThemeSchemaGeneratorTests if (linkedPublish) { options["build_property.AtomUILinkedPublish"] = "true"; + options["build_property.AtomUIRegistrationPackageId"] = + compilation.AssemblyName ?? "ThemeSchemaGeneratorTests"; } AnalyzerConfigOptionsProvider? optionsProvider = options.Count == 0 ? null : new TestAnalyzerConfigOptionsProvider(options); + var generators = linkedPublish + ? new ISourceGenerator[] + { + new TokenResourceKeyGenerator().AsSourceGenerator(), + new LinkedRegistrationPackageManifestGenerator().AsSourceGenerator() + } + : [new TokenResourceKeyGenerator().AsSourceGenerator()]; var driver = CSharpGeneratorDriver.Create( - [new TokenResourceKeyGenerator().AsSourceGenerator()], + generators, additionalTexts.ToImmutableArray(), (CSharpParseOptions)compilation.SyntaxTrees[0].Options, optionsProvider); diff --git a/tests/AtomUI.LinkedRegistration.Fixtures/Minimal/Minimal.csproj b/tests/AtomUI.LinkedRegistration.Fixtures/Minimal/Minimal.csproj index 30b7bfcf3..cbd0f9543 100644 --- a/tests/AtomUI.LinkedRegistration.Fixtures/Minimal/Minimal.csproj +++ b/tests/AtomUI.LinkedRegistration.Fixtures/Minimal/Minimal.csproj @@ -7,6 +7,11 @@ true + + true + GeneratedFiles + + ? includeIdentity = null, + Func, + IReadOnlyList>? selectAssets = null) + { + ArgumentNullException.ThrowIfNull(themeManagerBuilder); + ArgumentNullException.ThrowIfNull(controlThemesProvider); + } +} + public static class UnusedUnitFragment { public static void Add(AotTrimControlPackageRegistrationBuilder builder) { ArgumentNullException.ThrowIfNull(builder); - builder.TryEnterUnit("AtomUI.Desktop.Controls/FixtureUnused"); } } diff --git a/tests/AtomUIGallery.Tests/Workspace/GalleryDeveloperToolsConventionsTests.cs b/tests/AtomUIGallery.Tests/Workspace/GalleryDeveloperToolsConventionsTests.cs index 973241354..6cc684081 100644 --- a/tests/AtomUIGallery.Tests/Workspace/GalleryDeveloperToolsConventionsTests.cs +++ b/tests/AtomUIGallery.Tests/Workspace/GalleryDeveloperToolsConventionsTests.cs @@ -123,6 +123,10 @@ public class GalleryDeveloperToolsConventionsTests { var project = XDocument.Load(GetRepoFile( "controlgallery/AtomUIGallery.Browser/AtomUIGallery.Browser.csproj")); + var runAot = project.Descendants("RunAOTCompilation").ShouldHaveSingleItem(); + ((string?)runAot.Attribute("Condition")).ShouldBe("'$(WasmBuildingForNestedPublish)' == 'true'"); + runAot.Value.ShouldBe("false"); + var target = project.Descendants("Target") .Single(element => (string?)element.Attribute("Name") == @@ -132,6 +136,7 @@ public class GalleryDeveloperToolsConventionsTests var condition = (string?)target.Attribute("Condition"); condition.ShouldNotBeNull(); condition.ShouldContain("WasmBuildingForNestedPublish"); + condition.ShouldContain("RunAOTCompilation"); condition.ShouldContain("AtomUIBrowserNativeBuild"); target.Descendants("WasmBuildNative").ShouldHaveSingleItem().Value.ShouldBe("false"); target.Descendants("WasmEnableWebcil").ShouldHaveSingleItem().Value.ShouldBe("true");