From c5a33ed571fcc22cc10421f81674453ac2ffe7ac Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 3 Apr 2026 23:48:24 +0800
Subject: [PATCH 1/3] =?UTF-8?q?docs(source-generators):=20=E6=B7=BB?=
=?UTF-8?q?=E5=8A=A0=E6=BA=90=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8?=
=?UTF-8?q?=E6=96=87=E6=A1=A3=E5=92=8C=E6=B5=8B=E8=AF=95=E9=A1=B9=E7=9B=AE?=
=?UTF-8?q?=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 Context Get 注入生成器详细文档,包含使用示例和诊断信息
- 添加源代码生成器总览文档,涵盖 Log、Config Schema、ContextAware 等功能
- 配置测试项目 GFramework.SourceGenerators.Tests 的项目文件和依赖
- 生成器诊断规则新增至 Unshipped 分析器发布跟踪文件
---
.../ContextRegistrationAnalyzerTests.cs | 372 +++++++
.../Core/AnalyzerTest.cs | 34 +
.../GFramework.SourceGenerators.Tests.csproj | 1 +
.../AnalyzerReleases.Unshipped.md | 3 +
.../Analyzers/ContextRegistrationAnalyzer.cs | 907 ++++++++++++++++++
.../ContextRegistrationDiagnostics.cs | 44 +
GFramework.SourceGenerators/README.md | 11 +
.../context-get-generator.md | 27 +
docs/zh-CN/source-generators/index.md | 9 +-
9 files changed, 1405 insertions(+), 3 deletions(-)
create mode 100644 GFramework.SourceGenerators.Tests/Analyzers/ContextRegistrationAnalyzerTests.cs
create mode 100644 GFramework.SourceGenerators.Tests/Core/AnalyzerTest.cs
create mode 100644 GFramework.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs
create mode 100644 GFramework.SourceGenerators/Diagnostics/ContextRegistrationDiagnostics.cs
diff --git a/GFramework.SourceGenerators.Tests/Analyzers/ContextRegistrationAnalyzerTests.cs b/GFramework.SourceGenerators.Tests/Analyzers/ContextRegistrationAnalyzerTests.cs
new file mode 100644
index 00000000..7db229f7
--- /dev/null
+++ b/GFramework.SourceGenerators.Tests/Analyzers/ContextRegistrationAnalyzerTests.cs
@@ -0,0 +1,372 @@
+using GFramework.SourceGenerators.Analyzers;
+using GFramework.SourceGenerators.Tests.Core;
+
+namespace GFramework.SourceGenerators.Tests.Analyzers;
+
+///
+/// 验证 Context Get 注册可见性分析器的核心行为。
+///
+public sealed class ContextRegistrationAnalyzerTests
+{
+ private const string TestPreamble = """
+ using System;
+ using System.Collections.Generic;
+
+ namespace GFramework.Core.Abstractions.Rule
+ {
+ public interface IContextAware { }
+ }
+
+ namespace GFramework.Core.Abstractions.Model
+ {
+ public interface IModel : GFramework.Core.Abstractions.Rule.IContextAware { }
+ }
+
+ namespace GFramework.Core.Abstractions.Systems
+ {
+ public interface ISystem : GFramework.Core.Abstractions.Rule.IContextAware { }
+ }
+
+ namespace GFramework.Core.Abstractions.Utility
+ {
+ public interface IUtility : GFramework.Core.Abstractions.Rule.IContextAware { }
+ }
+
+ namespace GFramework.Core.Abstractions.Architectures
+ {
+ public interface IArchitecture
+ {
+ T RegisterModel(T model) where T : GFramework.Core.Abstractions.Model.IModel;
+ void RegisterModel(Action onCreated = null) where T : class, GFramework.Core.Abstractions.Model.IModel;
+ T RegisterSystem(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
+ void RegisterSystem(Action onCreated = null) where T : class, GFramework.Core.Abstractions.Systems.ISystem;
+ T RegisterUtility(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility;
+ void RegisterUtility(Action onCreated = null) where T : class, GFramework.Core.Abstractions.Utility.IUtility;
+ IArchitectureModule InstallModule(IArchitectureModule module);
+ }
+
+ public interface IArchitectureModule
+ {
+ void Install(IArchitecture architecture);
+ }
+
+ public interface IArchitectureContext
+ {
+ TModel GetModel() where TModel : class, GFramework.Core.Abstractions.Model.IModel;
+ IReadOnlyList GetModels() where TModel : class, GFramework.Core.Abstractions.Model.IModel;
+ TSystem GetSystem() where TSystem : class, GFramework.Core.Abstractions.Systems.ISystem;
+ IReadOnlyList GetSystems() where TSystem : class, GFramework.Core.Abstractions.Systems.ISystem;
+ TUtility GetUtility() where TUtility : class, GFramework.Core.Abstractions.Utility.IUtility;
+ IReadOnlyList GetUtilities() where TUtility : class, GFramework.Core.Abstractions.Utility.IUtility;
+ }
+ }
+
+ namespace GFramework.Core.Architectures
+ {
+ public abstract class Architecture : GFramework.Core.Abstractions.Architectures.IArchitecture
+ {
+ protected abstract void OnInitialize();
+
+ public virtual T RegisterModel(T model) where T : GFramework.Core.Abstractions.Model.IModel => model;
+
+ public virtual void RegisterModel(Action onCreated = null)
+ where T : class, GFramework.Core.Abstractions.Model.IModel
+ {
+ }
+
+ public virtual T RegisterSystem(T system) where T : GFramework.Core.Abstractions.Systems.ISystem => system;
+
+ public virtual void RegisterSystem(Action onCreated = null)
+ where T : class, GFramework.Core.Abstractions.Systems.ISystem
+ {
+ }
+
+ public virtual T RegisterUtility(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility => utility;
+
+ public virtual void RegisterUtility(Action onCreated = null)
+ where T : class, GFramework.Core.Abstractions.Utility.IUtility
+ {
+ }
+
+ public virtual GFramework.Core.Abstractions.Architectures.IArchitectureModule InstallModule(
+ GFramework.Core.Abstractions.Architectures.IArchitectureModule module)
+ {
+ module.Install(this);
+ return module;
+ }
+ }
+ }
+
+ namespace GFramework.Core.Extensions
+ {
+ public static class ContextAwareServiceExtensions
+ {
+ public static TModel GetModel(this GFramework.Core.Abstractions.Rule.IContextAware contextAware)
+ where TModel : class, GFramework.Core.Abstractions.Model.IModel => throw new NotImplementedException();
+
+ public static IReadOnlyList GetModels(this GFramework.Core.Abstractions.Rule.IContextAware contextAware)
+ where TModel : class, GFramework.Core.Abstractions.Model.IModel => throw new NotImplementedException();
+
+ public static TSystem GetSystem(this GFramework.Core.Abstractions.Rule.IContextAware contextAware)
+ where TSystem : class, GFramework.Core.Abstractions.Systems.ISystem => throw new NotImplementedException();
+
+ public static IReadOnlyList GetSystems(this GFramework.Core.Abstractions.Rule.IContextAware contextAware)
+ where TSystem : class, GFramework.Core.Abstractions.Systems.ISystem => throw new NotImplementedException();
+
+ public static TUtility GetUtility(this GFramework.Core.Abstractions.Rule.IContextAware contextAware)
+ where TUtility : class, GFramework.Core.Abstractions.Utility.IUtility => throw new NotImplementedException();
+
+ public static IReadOnlyList GetUtilities(this GFramework.Core.Abstractions.Rule.IContextAware contextAware)
+ where TUtility : class, GFramework.Core.Abstractions.Utility.IUtility => throw new NotImplementedException();
+ }
+ }
+
+ namespace GFramework.SourceGenerators.Abstractions.Rule
+ {
+ public sealed class GetModelAttribute : Attribute { }
+ public sealed class GetModelsAttribute : Attribute { }
+ public sealed class GetSystemAttribute : Attribute { }
+ public sealed class GetSystemsAttribute : Attribute { }
+ public sealed class GetUtilityAttribute : Attribute { }
+ public sealed class GetUtilitiesAttribute : Attribute { }
+ }
+ """;
+
+ [Test]
+ public async Task Reports_Warning_When_FieldInjectedModel_Is_Not_Registered()
+ {
+ var markup = MarkupTestSource.Parse(
+ Wrap("""
+ namespace TestApp
+ {
+ using GFramework.Core.Abstractions.Model;
+ using GFramework.Core.Abstractions.Systems;
+ using GFramework.Core.Architectures;
+ using GFramework.SourceGenerators.Abstractions.Rule;
+
+ public interface IInventoryModel : IModel { }
+
+ public sealed class InventoryPanelSystem : ISystem
+ {
+ [GetModel]
+ private IInventoryModel {|#0:_model|} = null!;
+ }
+
+ public sealed class GameArchitecture : Architecture
+ {
+ protected override void OnInitialize()
+ {
+ RegisterSystem(new InventoryPanelSystem());
+ }
+ }
+ }
+ """));
+
+ await AnalyzerTestDriver.RunAsync(
+ markup.Source,
+ markup.WithSpan(
+ new DiagnosticResult("GF_ContextRegistration_001", DiagnosticSeverity.Warning)
+ .WithArguments("IInventoryModel", "InventoryPanelSystem", "GameArchitecture"),
+ "0"));
+ }
+
+ [Test]
+ public async Task Does_Not_Report_When_FieldInjectedModel_Is_Registered()
+ {
+ await AnalyzerTestDriver.RunAsync(
+ Wrap("""
+ namespace TestApp
+ {
+ using GFramework.Core.Abstractions.Model;
+ using GFramework.Core.Abstractions.Systems;
+ using GFramework.Core.Architectures;
+ using GFramework.SourceGenerators.Abstractions.Rule;
+
+ public interface IInventoryModel : IModel { }
+
+ public sealed class InventoryModel : IInventoryModel { }
+
+ public sealed class InventoryPanelSystem : ISystem
+ {
+ [GetModel]
+ private IInventoryModel _model = null!;
+ }
+
+ public sealed class GameArchitecture : Architecture
+ {
+ protected override void OnInitialize()
+ {
+ RegisterModel(new InventoryModel());
+ RegisterSystem(new InventoryPanelSystem());
+ }
+ }
+ }
+ """));
+ }
+
+ [Test]
+ public async Task Reports_Warning_When_HandWrittenGetSystem_Call_Has_No_Registration()
+ {
+ var markup = MarkupTestSource.Parse(
+ Wrap("""
+ namespace TestApp
+ {
+ using GFramework.Core.Abstractions.Systems;
+ using GFramework.Core.Abstractions.Utility;
+ using GFramework.Core.Architectures;
+ using GFramework.Core.Extensions;
+
+ public interface ICombatSystem : ISystem { }
+
+ public sealed class UiUtility : IUtility
+ {
+ public void Initialize()
+ {
+ {|#0:this.GetSystem()|};
+ }
+ }
+
+ public sealed class GameArchitecture : Architecture
+ {
+ protected override void OnInitialize()
+ {
+ RegisterUtility(new UiUtility());
+ }
+ }
+ }
+ """));
+
+ await AnalyzerTestDriver.RunAsync(
+ markup.Source,
+ markup.WithSpan(
+ new DiagnosticResult("GF_ContextRegistration_002", DiagnosticSeverity.Warning)
+ .WithArguments("ICombatSystem", "UiUtility", "GameArchitecture"),
+ "0"));
+ }
+
+ [Test]
+ public async Task Does_Not_Report_When_Registration_Comes_From_Installed_Module()
+ {
+ await AnalyzerTestDriver.RunAsync(
+ Wrap("""
+ namespace TestApp
+ {
+ using GFramework.Core.Abstractions.Architectures;
+ using GFramework.Core.Abstractions.Model;
+ using GFramework.Core.Abstractions.Systems;
+ using GFramework.Core.Architectures;
+ using GFramework.SourceGenerators.Abstractions.Rule;
+
+ public interface IInventoryModel : IModel { }
+
+ public sealed class InventoryModel : IInventoryModel { }
+
+ public sealed class InventoryPanelSystem : ISystem
+ {
+ [GetModel]
+ private IInventoryModel _model = null!;
+ }
+
+ public sealed class InventoryModule : IArchitectureModule
+ {
+ public void Install(IArchitecture architecture)
+ {
+ architecture.RegisterModel(new InventoryModel());
+ }
+ }
+
+ public sealed class GameArchitecture : Architecture
+ {
+ protected override void OnInitialize()
+ {
+ InstallModule(new InventoryModule());
+ RegisterSystem(new InventoryPanelSystem());
+ }
+ }
+ }
+ """));
+ }
+
+ [Test]
+ public async Task Does_Not_Report_When_Owning_Architecture_Cannot_Be_Uniquely_Determined()
+ {
+ await AnalyzerTestDriver.RunAsync(
+ Wrap("""
+ namespace TestApp
+ {
+ using GFramework.Core.Abstractions.Model;
+ using GFramework.Core.Abstractions.Systems;
+ using GFramework.Core.Architectures;
+ using GFramework.SourceGenerators.Abstractions.Rule;
+
+ public interface IInventoryModel : IModel { }
+
+ public sealed class InventoryPanelSystem : ISystem
+ {
+ [GetModel]
+ private IInventoryModel _model = null!;
+ }
+
+ public sealed class FirstArchitecture : Architecture
+ {
+ protected override void OnInitialize()
+ {
+ RegisterSystem(new InventoryPanelSystem());
+ }
+ }
+
+ public sealed class SecondArchitecture : Architecture
+ {
+ protected override void OnInitialize()
+ {
+ RegisterSystem(new InventoryPanelSystem());
+ }
+ }
+ }
+ """));
+ }
+
+ [Test]
+ public async Task Reports_Warning_When_GetUtilities_Field_Has_No_Registered_Utility()
+ {
+ var markup = MarkupTestSource.Parse(
+ Wrap("""
+ namespace TestApp
+ {
+ using System.Collections.Generic;
+ using GFramework.Core.Abstractions.Systems;
+ using GFramework.Core.Abstractions.Utility;
+ using GFramework.Core.Architectures;
+ using GFramework.SourceGenerators.Abstractions.Rule;
+
+ public interface IInventoryUtility : IUtility { }
+
+ public sealed class InventoryPanelSystem : ISystem
+ {
+ [GetUtilities]
+ private IReadOnlyList {|#0:_utilities|} = null!;
+ }
+
+ public sealed class GameArchitecture : Architecture
+ {
+ protected override void OnInitialize()
+ {
+ RegisterSystem(new InventoryPanelSystem());
+ }
+ }
+ }
+ """));
+
+ await AnalyzerTestDriver.RunAsync(
+ markup.Source,
+ markup.WithSpan(
+ new DiagnosticResult("GF_ContextRegistration_003", DiagnosticSeverity.Warning)
+ .WithArguments("IInventoryUtility", "InventoryPanelSystem", "GameArchitecture"),
+ "0"));
+ }
+
+ private static string Wrap(string source)
+ {
+ return $"{TestPreamble}{Environment.NewLine}{Environment.NewLine}{source}";
+ }
+}
diff --git a/GFramework.SourceGenerators.Tests/Core/AnalyzerTest.cs b/GFramework.SourceGenerators.Tests/Core/AnalyzerTest.cs
new file mode 100644
index 00000000..9d9a0b73
--- /dev/null
+++ b/GFramework.SourceGenerators.Tests/Core/AnalyzerTest.cs
@@ -0,0 +1,34 @@
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace GFramework.SourceGenerators.Tests.Core;
+
+///
+/// 提供 Roslyn 分析器测试的通用运行入口。
+///
+/// 要验证的分析器类型。
+public static class AnalyzerTestDriver
+ where TAnalyzer : DiagnosticAnalyzer, new()
+{
+ ///
+ /// 运行分析器测试并断言期望诊断。
+ ///
+ /// 测试输入源码。
+ /// 期望诊断集合。
+ /// 异步测试任务。
+ public static async Task RunAsync(
+ string source,
+ params DiagnosticResult[] diagnostics)
+ {
+ var test = new CSharpAnalyzerTest
+ {
+ TestState =
+ {
+ Sources = { source }
+ },
+ DisabledDiagnostics = { "GF_Common_Trace_001" }
+ };
+
+ test.ExpectedDiagnostics.AddRange(diagnostics);
+ await test.RunAsync();
+ }
+}
diff --git a/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj b/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj
index 28f0aec9..bb4c692c 100644
--- a/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj
+++ b/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj
@@ -13,6 +13,7 @@
+
diff --git a/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md
index 356e4cbe..72d01b30 100644
--- a/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md
+++ b/GFramework.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -15,6 +15,9 @@
GF_ContextGet_006 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
GF_ContextGet_007 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
GF_ContextGet_008 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
+ GF_ContextRegistration_001 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
+ GF_ContextRegistration_002 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
+ GF_ContextRegistration_003 | GFramework.SourceGenerators.rule | Warning | ContextRegistrationDiagnostics
GF_ConfigSchema_001 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_002 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
GF_ConfigSchema_003 | GFramework.SourceGenerators.Config | Error | ConfigSchemaDiagnostics
diff --git a/GFramework.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs b/GFramework.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs
new file mode 100644
index 00000000..5d20f79e
--- /dev/null
+++ b/GFramework.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs
@@ -0,0 +1,907 @@
+using System.Collections.Immutable;
+using GFramework.SourceGenerators.Common.Constants;
+using GFramework.SourceGenerators.Diagnostics;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+namespace GFramework.SourceGenerators.Analyzers;
+
+///
+/// 分析 Context Get 使用点是否能在所属架构中找到静态可见的 Model、System、Utility 注册。
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class ContextRegistrationAnalyzer : DiagnosticAnalyzer
+{
+ ///
+ /// 当前分析器支持的诊断规则。
+ ///
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(
+ ContextRegistrationDiagnostics.ModelRegistrationMissing,
+ ContextRegistrationDiagnostics.SystemRegistrationMissing,
+ ContextRegistrationDiagnostics.UtilityRegistrationMissing);
+
+ ///
+ /// 初始化分析器并注册字段注入与手写 GetX 调用分析。
+ ///
+ /// 分析器上下文。
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(static compilationContext =>
+ {
+ var symbols = SymbolCache.Create(compilationContext.Compilation);
+ if (!symbols.IsReady)
+ return;
+
+ var registrationIndex = new Lazy(
+ () => RegistrationIndex.Build(compilationContext.Compilation, symbols),
+ LazyThreadSafetyMode.ExecutionAndPublication);
+
+ compilationContext.RegisterSyntaxNodeAction(
+ syntaxContext => AnalyzeField(syntaxContext, symbols, registrationIndex.Value),
+ SyntaxKind.VariableDeclarator);
+
+ compilationContext.RegisterOperationAction(
+ operationContext => AnalyzeInvocation(operationContext, symbols, registrationIndex.Value),
+ OperationKind.Invocation);
+ });
+ }
+
+ private static void AnalyzeField(
+ SyntaxNodeAnalysisContext context,
+ SymbolCache symbols,
+ RegistrationIndex registrationIndex)
+ {
+ if (context.Node is not VariableDeclaratorSyntax variableDeclarator)
+ return;
+
+ if (context.SemanticModel.GetDeclaredSymbol(variableDeclarator, context.CancellationToken) is not IFieldSymbol fieldSymbol)
+ return;
+
+ if (!TryCreateBindingRequest(fieldSymbol, variableDeclarator.Identifier.GetLocation(), symbols, out var request))
+ return;
+
+ ReportMissingRegistration(context, registrationIndex, request);
+ }
+
+ private static void AnalyzeInvocation(
+ OperationAnalysisContext context,
+ SymbolCache symbols,
+ RegistrationIndex registrationIndex)
+ {
+ if (context.Operation is not IInvocationOperation invocation)
+ return;
+
+ if (!TryCreateBindingRequest(invocation, context.ContainingSymbol?.ContainingType, symbols, out var request))
+ return;
+
+ ReportMissingRegistration(context, registrationIndex, request);
+ }
+
+ private static void ReportMissingRegistration(
+ SyntaxNodeAnalysisContext context,
+ RegistrationIndex registrationIndex,
+ BindingRequest request)
+ {
+ if (!registrationIndex.TryGetOwningArchitecture(request.OwnerType, out var architectureType))
+ return;
+
+ if (registrationIndex.HasRegistration(architectureType, request.Kind, request.ServiceType))
+ return;
+
+ context.ReportDiagnostic(CreateMissingRegistrationDiagnostic(request, architectureType));
+ }
+
+ private static void ReportMissingRegistration(
+ OperationAnalysisContext context,
+ RegistrationIndex registrationIndex,
+ BindingRequest request)
+ {
+ if (!registrationIndex.TryGetOwningArchitecture(request.OwnerType, out var architectureType))
+ return;
+
+ if (registrationIndex.HasRegistration(architectureType, request.Kind, request.ServiceType))
+ return;
+
+ context.ReportDiagnostic(CreateMissingRegistrationDiagnostic(request, architectureType));
+ }
+
+ private static Diagnostic CreateMissingRegistrationDiagnostic(
+ BindingRequest request,
+ INamedTypeSymbol architectureType)
+ {
+ return Diagnostic.Create(
+ request.Kind switch
+ {
+ ComponentKind.Model => ContextRegistrationDiagnostics.ModelRegistrationMissing,
+ ComponentKind.System => ContextRegistrationDiagnostics.SystemRegistrationMissing,
+ ComponentKind.Utility => ContextRegistrationDiagnostics.UtilityRegistrationMissing,
+ _ => throw new ArgumentOutOfRangeException(nameof(request))
+ },
+ request.Location,
+ request.ServiceType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
+ request.OwnerType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
+ architectureType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
+ }
+
+ private static bool TryCreateBindingRequest(
+ IFieldSymbol fieldSymbol,
+ Location location,
+ SymbolCache symbols,
+ out BindingRequest request)
+ {
+ request = default;
+
+ if (fieldSymbol.ContainingType is not INamedTypeSymbol ownerType)
+ return false;
+
+ foreach (var attribute in fieldSymbol.GetAttributes())
+ {
+ if (!TryMapAttribute(attribute.AttributeClass, symbols, out var componentKind, out var expectsCollection))
+ continue;
+
+ var serviceType = expectsCollection
+ ? TryGetCollectionElementType(fieldSymbol.Type, symbols)
+ : fieldSymbol.Type as INamedTypeSymbol;
+
+ if (serviceType == null)
+ return false;
+
+ request = new BindingRequest(componentKind, ownerType, serviceType, location);
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryCreateBindingRequest(
+ IInvocationOperation invocation,
+ INamedTypeSymbol? ownerType,
+ SymbolCache symbols,
+ out BindingRequest request)
+ {
+ request = default;
+
+ var targetMethod = invocation.TargetMethod;
+ if (!targetMethod.IsGenericMethod || targetMethod.TypeArguments.Length != 1)
+ return false;
+
+ if (invocation.Syntax.SyntaxTree.FilePath.EndsWith(".g.cs", StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ if (targetMethod.TypeArguments[0] is not INamedTypeSymbol serviceType)
+ return false;
+
+ if (_contextAwareBindingNames.TryGetValue(targetMethod.Name, out var modelKind))
+ {
+ if (!IsSupportedGetInvocationTarget(targetMethod, symbols))
+ return false;
+ }
+ else
+ {
+ return false;
+ }
+
+ if (ownerType == null)
+ return false;
+
+ request = new BindingRequest(modelKind, ownerType, serviceType, invocation.Syntax.GetLocation());
+ return true;
+ }
+
+ private static bool TryMapAttribute(
+ INamedTypeSymbol? attributeType,
+ SymbolCache symbols,
+ out ComponentKind componentKind,
+ out bool expectsCollection)
+ {
+ componentKind = default;
+ expectsCollection = false;
+
+ if (attributeType == null)
+ return false;
+
+ if (SymbolEqualityComparer.Default.Equals(attributeType, symbols.GetModelAttribute))
+ {
+ componentKind = ComponentKind.Model;
+ return true;
+ }
+
+ if (SymbolEqualityComparer.Default.Equals(attributeType, symbols.GetModelsAttribute))
+ {
+ componentKind = ComponentKind.Model;
+ expectsCollection = true;
+ return true;
+ }
+
+ if (SymbolEqualityComparer.Default.Equals(attributeType, symbols.GetSystemAttribute))
+ {
+ componentKind = ComponentKind.System;
+ return true;
+ }
+
+ if (SymbolEqualityComparer.Default.Equals(attributeType, symbols.GetSystemsAttribute))
+ {
+ componentKind = ComponentKind.System;
+ expectsCollection = true;
+ return true;
+ }
+
+ if (SymbolEqualityComparer.Default.Equals(attributeType, symbols.GetUtilityAttribute))
+ {
+ componentKind = ComponentKind.Utility;
+ return true;
+ }
+
+ if (SymbolEqualityComparer.Default.Equals(attributeType, symbols.GetUtilitiesAttribute))
+ {
+ componentKind = ComponentKind.Utility;
+ expectsCollection = true;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool IsSupportedGetInvocationTarget(
+ IMethodSymbol targetMethod,
+ SymbolCache symbols)
+ {
+ if (targetMethod.ContainingType == null)
+ return false;
+
+ if (SymbolEqualityComparer.Default.Equals(targetMethod.ContainingType, symbols.ContextAwareServiceExtensions))
+ return true;
+
+ return SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureContext);
+ }
+
+ private static INamedTypeSymbol? TryGetCollectionElementType(
+ ITypeSymbol fieldType,
+ SymbolCache symbols)
+ {
+ if (fieldType is not INamedTypeSymbol namedType)
+ return null;
+
+ if (!SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, symbols.IReadOnlyList))
+ return null;
+
+ return namedType.TypeArguments[0] as INamedTypeSymbol;
+ }
+
+ private static readonly IReadOnlyDictionary _contextAwareBindingNames =
+ new Dictionary(StringComparer.Ordinal)
+ {
+ ["GetModel"] = ComponentKind.Model,
+ ["GetModels"] = ComponentKind.Model,
+ ["GetSystem"] = ComponentKind.System,
+ ["GetSystems"] = ComponentKind.System,
+ ["GetUtility"] = ComponentKind.Utility,
+ ["GetUtilities"] = ComponentKind.Utility
+ };
+
+ private enum ComponentKind
+ {
+ Model,
+ System,
+ Utility
+ }
+
+ private readonly record struct BindingRequest(
+ ComponentKind Kind,
+ INamedTypeSymbol OwnerType,
+ INamedTypeSymbol ServiceType,
+ Location Location);
+
+ private sealed class SymbolCache
+ {
+ private SymbolCache(
+ INamedTypeSymbol? architectureType,
+ INamedTypeSymbol? iArchitectureType,
+ INamedTypeSymbol? iArchitectureModuleType,
+ INamedTypeSymbol? iArchitectureContextType,
+ INamedTypeSymbol? iReadOnlyListType,
+ INamedTypeSymbol? contextAwareServiceExtensionsType,
+ INamedTypeSymbol? getModelAttribute,
+ INamedTypeSymbol? getModelsAttribute,
+ INamedTypeSymbol? getSystemAttribute,
+ INamedTypeSymbol? getSystemsAttribute,
+ INamedTypeSymbol? getUtilityAttribute,
+ INamedTypeSymbol? getUtilitiesAttribute)
+ {
+ ArchitectureType = architectureType;
+ IArchitectureType = iArchitectureType;
+ IArchitectureModuleType = iArchitectureModuleType;
+ IArchitectureContext = iArchitectureContextType;
+ IReadOnlyList = iReadOnlyListType;
+ ContextAwareServiceExtensions = contextAwareServiceExtensionsType;
+ GetModelAttribute = getModelAttribute;
+ GetModelsAttribute = getModelsAttribute;
+ GetSystemAttribute = getSystemAttribute;
+ GetSystemsAttribute = getSystemsAttribute;
+ GetUtilityAttribute = getUtilityAttribute;
+ GetUtilitiesAttribute = getUtilitiesAttribute;
+ }
+
+ public INamedTypeSymbol? ArchitectureType { get; }
+
+ public INamedTypeSymbol? IArchitectureType { get; }
+
+ public INamedTypeSymbol? IArchitectureModuleType { get; }
+
+ public INamedTypeSymbol? IArchitectureContext { get; }
+
+ public INamedTypeSymbol? IReadOnlyList { get; }
+
+ public INamedTypeSymbol? ContextAwareServiceExtensions { get; }
+
+ public INamedTypeSymbol? GetModelAttribute { get; }
+
+ public INamedTypeSymbol? GetModelsAttribute { get; }
+
+ public INamedTypeSymbol? GetSystemAttribute { get; }
+
+ public INamedTypeSymbol? GetSystemsAttribute { get; }
+
+ public INamedTypeSymbol? GetUtilityAttribute { get; }
+
+ public INamedTypeSymbol? GetUtilitiesAttribute { get; }
+
+ public bool IsReady =>
+ ArchitectureType != null &&
+ IArchitectureType != null &&
+ IArchitectureModuleType != null &&
+ IArchitectureContext != null &&
+ IReadOnlyList != null &&
+ ContextAwareServiceExtensions != null &&
+ GetModelAttribute != null &&
+ GetModelsAttribute != null &&
+ GetSystemAttribute != null &&
+ GetSystemsAttribute != null &&
+ GetUtilityAttribute != null &&
+ GetUtilitiesAttribute != null;
+
+ public static SymbolCache Create(Compilation compilation)
+ {
+ return new SymbolCache(
+ compilation.GetTypeByMetadataName($"{PathContests.CoreNamespace}.Architectures.Architecture"),
+ compilation.GetTypeByMetadataName($"{PathContests.CoreAbstractionsNamespace}.Architectures.IArchitecture"),
+ compilation.GetTypeByMetadataName($"{PathContests.CoreAbstractionsNamespace}.Architectures.IArchitectureModule"),
+ compilation.GetTypeByMetadataName($"{PathContests.CoreAbstractionsNamespace}.Architectures.IArchitectureContext"),
+ compilation.GetTypeByMetadataName("System.Collections.Generic.IReadOnlyList`1"),
+ compilation.GetTypeByMetadataName($"{PathContests.CoreNamespace}.Extensions.ContextAwareServiceExtensions"),
+ compilation.GetTypeByMetadataName($"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetModelAttribute"),
+ compilation.GetTypeByMetadataName($"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetModelsAttribute"),
+ compilation.GetTypeByMetadataName($"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetSystemAttribute"),
+ compilation.GetTypeByMetadataName($"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetSystemsAttribute"),
+ compilation.GetTypeByMetadataName($"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetUtilityAttribute"),
+ compilation.GetTypeByMetadataName($"{PathContests.SourceGeneratorsAbstractionsPath}.Rule.GetUtilitiesAttribute"));
+ }
+ }
+
+ private sealed class RegistrationIndex
+ {
+ private RegistrationIndex(
+ Compilation compilation,
+ IReadOnlyDictionary registrations)
+ {
+ _compilation = compilation;
+ _registrations = registrations;
+ }
+
+ private readonly Compilation _compilation;
+ private readonly IReadOnlyDictionary _registrations;
+
+ public static RegistrationIndex Build(
+ Compilation compilation,
+ SymbolCache symbols)
+ {
+ var registrations = new Dictionary(SymbolEqualityComparer.Default);
+
+ foreach (var type in SymbolHelpers.EnumerateNamedTypes(compilation.Assembly.GlobalNamespace))
+ {
+ if (!SymbolHelpers.IsAssignableTo(type, symbols.ArchitectureType))
+ continue;
+
+ if (type.IsAbstract)
+ continue;
+
+ var data = AnalyzeArchitecture(compilation, symbols, type);
+ if (data.IsEmpty)
+ continue;
+
+ registrations[type] = data;
+ }
+
+ return new RegistrationIndex(compilation, registrations);
+ }
+
+ public bool TryGetOwningArchitecture(
+ INamedTypeSymbol ownerType,
+ out INamedTypeSymbol architectureType)
+ {
+ architectureType = default!;
+
+ if (_registrations.ContainsKey(ownerType))
+ {
+ architectureType = ownerType;
+ return true;
+ }
+
+ INamedTypeSymbol? candidate = null;
+ foreach (var pair in _registrations)
+ {
+ if (!pair.Value.ContainsOwner(ownerType, _compilation))
+ continue;
+
+ if (candidate != null)
+ return false;
+
+ candidate = pair.Key;
+ }
+
+ if (candidate == null)
+ return false;
+
+ architectureType = candidate;
+ return true;
+ }
+
+ public bool HasRegistration(
+ INamedTypeSymbol architectureType,
+ ComponentKind componentKind,
+ INamedTypeSymbol serviceType)
+ {
+ if (!_registrations.TryGetValue(architectureType, out var data))
+ return false;
+
+ return data.HasRegistration(componentKind, serviceType, _compilation);
+ }
+
+ private static ArchitectureRegistrationData AnalyzeArchitecture(
+ Compilation compilation,
+ SymbolCache symbols,
+ INamedTypeSymbol architectureType)
+ {
+ var data = new ArchitectureRegistrationData();
+ var visitedMethods = new HashSet(SymbolEqualityComparer.Default);
+ var pendingMethods = new Queue();
+ var visitedModules = new HashSet(SymbolEqualityComparer.Default);
+
+ foreach (var rootMethod in GetArchitectureRootMethods(architectureType))
+ pendingMethods.Enqueue(rootMethod);
+
+ while (pendingMethods.Count > 0)
+ {
+ var method = pendingMethods.Dequeue();
+ if (!visitedMethods.Add(method))
+ continue;
+
+ foreach (var invocation in SymbolHelpers.GetInvocationOperations(method, compilation))
+ {
+ if (TryGetRegistration(invocation, symbols, out var kind, out var registeredType))
+ {
+ data.Add(kind, registeredType);
+ continue;
+ }
+
+ if (TryGetInstalledModuleType(invocation, symbols, out var moduleType) &&
+ visitedModules.Add(moduleType))
+ {
+ AnalyzeModule(compilation, symbols, moduleType, data, visitedModules);
+ continue;
+ }
+
+ if (TryResolveArchitectureHelperMethod(invocation.TargetMethod, architectureType, out var helperMethod))
+ pendingMethods.Enqueue(helperMethod);
+ }
+ }
+
+ return data;
+ }
+
+ private static void AnalyzeModule(
+ Compilation compilation,
+ SymbolCache symbols,
+ INamedTypeSymbol moduleType,
+ ArchitectureRegistrationData data,
+ ISet visitedModules)
+ {
+ var visitedMethods = new HashSet(SymbolEqualityComparer.Default);
+ var pendingMethods = new Queue();
+
+ foreach (var rootMethod in GetModuleRootMethods(moduleType))
+ pendingMethods.Enqueue(rootMethod);
+
+ while (pendingMethods.Count > 0)
+ {
+ var method = pendingMethods.Dequeue();
+ if (!visitedMethods.Add(method))
+ continue;
+
+ foreach (var invocation in SymbolHelpers.GetInvocationOperations(method, compilation))
+ {
+ if (TryGetRegistration(invocation, symbols, out var kind, out var registeredType))
+ {
+ data.Add(kind, registeredType);
+ continue;
+ }
+
+ if (TryGetInstalledModuleType(invocation, symbols, out var nestedModuleType) &&
+ visitedModules.Add(nestedModuleType))
+ {
+ AnalyzeModule(compilation, symbols, nestedModuleType, data, visitedModules);
+ continue;
+ }
+
+ if (TryResolveModuleHelperMethod(invocation.TargetMethod, moduleType, out var helperMethod))
+ pendingMethods.Enqueue(helperMethod);
+ }
+ }
+ }
+
+ private static IEnumerable GetArchitectureRootMethods(INamedTypeSymbol architectureType)
+ {
+ foreach (var type in SymbolHelpers.EnumerateTypeHierarchy(architectureType))
+ {
+ foreach (var member in type.GetMembers())
+ {
+ if (member is not IMethodSymbol method)
+ continue;
+
+ if (method.IsStatic)
+ continue;
+
+ if (method.Name is not ("OnInitialize" or "InstallModules"))
+ continue;
+
+ if (method.DeclaringSyntaxReferences.Length == 0)
+ continue;
+
+ yield return method;
+ }
+ }
+ }
+
+ private static IEnumerable GetModuleRootMethods(INamedTypeSymbol moduleType)
+ {
+ foreach (var type in SymbolHelpers.EnumerateTypeHierarchy(moduleType))
+ {
+ foreach (var member in type.GetMembers("Install").OfType())
+ {
+ if (member.IsStatic || member.Parameters.Length != 1)
+ continue;
+
+ if (member.DeclaringSyntaxReferences.Length == 0)
+ continue;
+
+ yield return member;
+ }
+ }
+ }
+
+ private static bool TryGetInstalledModuleType(
+ IInvocationOperation invocation,
+ SymbolCache symbols,
+ out INamedTypeSymbol moduleType)
+ {
+ moduleType = default!;
+
+ if (invocation.Arguments.Length == 0)
+ return false;
+
+ if (invocation.TargetMethod.Name is not ("InstallModule" or "InstallGodotModule"))
+ return false;
+
+ var candidateType = SymbolHelpers.TryGetCreatedType(invocation.Arguments[0].Value);
+ if (candidateType == null)
+ return false;
+
+ if (!SymbolHelpers.IsAssignableTo(candidateType, symbols.IArchitectureModuleType))
+ return false;
+
+ moduleType = candidateType;
+ return true;
+ }
+
+ private static bool TryGetRegistration(
+ IInvocationOperation invocation,
+ SymbolCache symbols,
+ out ComponentKind componentKind,
+ out INamedTypeSymbol registeredType)
+ {
+ componentKind = default;
+ registeredType = default!;
+
+ var targetMethod = invocation.TargetMethod;
+ if (!targetMethod.IsGenericMethod || targetMethod.TypeArguments.Length != 1)
+ return false;
+
+ if (targetMethod.TypeArguments[0] is not INamedTypeSymbol namedType)
+ return false;
+
+ if (targetMethod.Name == "RegisterModel" &&
+ SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType))
+ {
+ componentKind = ComponentKind.Model;
+ registeredType = namedType;
+ return true;
+ }
+
+ if (targetMethod.Name == "RegisterSystem" &&
+ SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType))
+ {
+ componentKind = ComponentKind.System;
+ registeredType = namedType;
+ return true;
+ }
+
+ if (targetMethod.Name == "RegisterUtility" &&
+ SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType))
+ {
+ componentKind = ComponentKind.Utility;
+ registeredType = namedType;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryResolveArchitectureHelperMethod(
+ IMethodSymbol targetMethod,
+ INamedTypeSymbol architectureType,
+ out IMethodSymbol helperMethod)
+ {
+ helperMethod = default!;
+
+ if (targetMethod.MethodKind is not (MethodKind.Ordinary or MethodKind.Constructor))
+ return false;
+
+ if (!SymbolHelpers.IsWithinTypeHierarchy(targetMethod.ContainingType, architectureType))
+ return false;
+
+ // 对已经具备源码的方法保留原始目标,避免把显式的 base 调用重新折回到当前 override。
+ helperMethod = targetMethod.DeclaringSyntaxReferences.Length > 0 && !targetMethod.IsAbstract
+ ? targetMethod
+ : SymbolHelpers.ResolveHierarchyMethodImplementation(targetMethod, architectureType) ?? targetMethod;
+ return helperMethod.DeclaringSyntaxReferences.Length > 0;
+ }
+
+ private static bool TryResolveModuleHelperMethod(
+ IMethodSymbol targetMethod,
+ INamedTypeSymbol moduleType,
+ out IMethodSymbol helperMethod)
+ {
+ helperMethod = default!;
+
+ if (targetMethod.MethodKind is not (MethodKind.Ordinary or MethodKind.Constructor))
+ return false;
+
+ if (!SymbolHelpers.IsWithinTypeHierarchy(targetMethod.ContainingType, moduleType))
+ return false;
+
+ helperMethod = targetMethod.DeclaringSyntaxReferences.Length > 0 && !targetMethod.IsAbstract
+ ? targetMethod
+ : SymbolHelpers.ResolveHierarchyMethodImplementation(targetMethod, moduleType) ?? targetMethod;
+ return helperMethod.DeclaringSyntaxReferences.Length > 0;
+ }
+ }
+
+ private sealed class ArchitectureRegistrationData
+ {
+ private readonly HashSet _models = new(SymbolEqualityComparer.Default);
+ private readonly HashSet _systems = new(SymbolEqualityComparer.Default);
+ private readonly HashSet _utilities = new(SymbolEqualityComparer.Default);
+
+ public bool IsEmpty => _models.Count == 0 && _systems.Count == 0 && _utilities.Count == 0;
+
+ public void Add(ComponentKind componentKind, INamedTypeSymbol registeredType)
+ {
+ GetCollection(componentKind).Add(registeredType);
+ }
+
+ public bool HasRegistration(
+ ComponentKind componentKind,
+ INamedTypeSymbol requestedType,
+ Compilation compilation)
+ {
+ return GetCollection(componentKind).Any(candidate =>
+ SymbolHelpers.IsServiceCompatible(candidate, requestedType, compilation));
+ }
+
+ public bool ContainsOwner(
+ INamedTypeSymbol ownerType,
+ Compilation compilation)
+ {
+ return _models.Any(candidate => SymbolHelpers.IsOwnershipCompatible(ownerType, candidate, compilation)) ||
+ _systems.Any(candidate => SymbolHelpers.IsOwnershipCompatible(ownerType, candidate, compilation)) ||
+ _utilities.Any(candidate => SymbolHelpers.IsOwnershipCompatible(ownerType, candidate, compilation));
+ }
+
+ private ISet GetCollection(ComponentKind componentKind)
+ {
+ return componentKind switch
+ {
+ ComponentKind.Model => _models,
+ ComponentKind.System => _systems,
+ ComponentKind.Utility => _utilities,
+ _ => throw new ArgumentOutOfRangeException(nameof(componentKind))
+ };
+ }
+ }
+
+ private static class SymbolHelpers
+ {
+ public static IEnumerable EnumerateNamedTypes(INamespaceSymbol namespaceSymbol)
+ {
+ foreach (var namespaceMember in namespaceSymbol.GetNamespaceMembers())
+ {
+ foreach (var nestedType in EnumerateNamedTypes(namespaceMember))
+ yield return nestedType;
+ }
+
+ foreach (var typeMember in namespaceSymbol.GetTypeMembers())
+ {
+ yield return typeMember;
+
+ foreach (var nestedType in EnumerateNestedTypes(typeMember))
+ yield return nestedType;
+ }
+ }
+
+ public static IEnumerable EnumerateTypeHierarchy(INamedTypeSymbol type)
+ {
+ for (var current = type; current != null; current = current.BaseType)
+ yield return current;
+ }
+
+ public static bool IsAssignableTo(ITypeSymbol? fromType, ITypeSymbol? toType)
+ {
+ if (fromType == null || toType == null)
+ return false;
+
+ if (SymbolEqualityComparer.Default.Equals(fromType, toType))
+ return true;
+
+ return fromType.AllInterfaces.Any(interfaceType =>
+ SymbolEqualityComparer.Default.Equals(interfaceType, toType)) ||
+ EnumerateBaseTypes(fromType).Any(baseType =>
+ SymbolEqualityComparer.Default.Equals(baseType, toType));
+ }
+
+ public static bool IsWithinTypeHierarchy(
+ INamedTypeSymbol? candidateType,
+ INamedTypeSymbol ownerType)
+ {
+ if (candidateType == null)
+ return false;
+
+ return EnumerateTypeHierarchy(ownerType).Any(type =>
+ SymbolEqualityComparer.Default.Equals(type, candidateType));
+ }
+
+ public static IMethodSymbol? ResolveHierarchyMethodImplementation(
+ IMethodSymbol method,
+ INamedTypeSymbol ownerType)
+ {
+ foreach (var type in EnumerateTypeHierarchy(ownerType))
+ {
+ foreach (var candidate in type.GetMembers(method.Name).OfType())
+ {
+ if (!HasCompatibleSignature(candidate, method))
+ continue;
+
+ if (SymbolEqualityComparer.Default.Equals(candidate.OriginalDefinition, method.OriginalDefinition))
+ return candidate;
+
+ for (var overridden = candidate.OverriddenMethod; overridden != null; overridden = overridden.OverriddenMethod)
+ {
+ if (SymbolEqualityComparer.Default.Equals(overridden.OriginalDefinition, method.OriginalDefinition))
+ return candidate;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static IEnumerable GetInvocationOperations(
+ IMethodSymbol method,
+ Compilation compilation)
+ {
+ foreach (var syntaxReference in method.DeclaringSyntaxReferences)
+ {
+ var syntax = syntaxReference.GetSyntax();
+ var semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree);
+ var operation = syntax switch
+ {
+ MethodDeclarationSyntax methodDeclaration when methodDeclaration.Body != null =>
+ semanticModel.GetOperation(methodDeclaration.Body),
+ MethodDeclarationSyntax methodDeclaration when methodDeclaration.ExpressionBody != null =>
+ semanticModel.GetOperation(methodDeclaration.ExpressionBody.Expression),
+ ConstructorDeclarationSyntax constructorDeclaration when constructorDeclaration.Body != null =>
+ semanticModel.GetOperation(constructorDeclaration.Body),
+ ConstructorDeclarationSyntax constructorDeclaration when constructorDeclaration.ExpressionBody != null =>
+ semanticModel.GetOperation(constructorDeclaration.ExpressionBody.Expression),
+ _ => null
+ };
+
+ if (operation == null)
+ continue;
+
+ foreach (var invocation in operation.DescendantsAndSelf().OfType())
+ yield return invocation;
+ }
+ }
+
+ public static INamedTypeSymbol? TryGetCreatedType(IOperation operation)
+ {
+ var current = operation;
+
+ while (current is IConversionOperation conversionOperation)
+ current = conversionOperation.Operand;
+
+ return current switch
+ {
+ IObjectCreationOperation objectCreationOperation => objectCreationOperation.Type as INamedTypeSymbol,
+ _ => null
+ };
+ }
+
+ public static bool IsServiceCompatible(
+ INamedTypeSymbol registeredType,
+ INamedTypeSymbol requestedType,
+ Compilation compilation)
+ {
+ if (IsAssignableTo(registeredType, requestedType))
+ return true;
+
+ return compilation.ClassifyConversion(registeredType, requestedType).IsImplicit;
+ }
+
+ public static bool IsOwnershipCompatible(
+ INamedTypeSymbol ownerType,
+ INamedTypeSymbol registeredType,
+ Compilation compilation)
+ {
+ if (IsAssignableTo(ownerType, registeredType) || IsAssignableTo(registeredType, ownerType))
+ return true;
+
+ return compilation.ClassifyConversion(ownerType, registeredType).IsImplicit ||
+ compilation.ClassifyConversion(registeredType, ownerType).IsImplicit;
+ }
+
+ private static IEnumerable EnumerateNestedTypes(INamedTypeSymbol type)
+ {
+ foreach (var nestedType in type.GetTypeMembers())
+ {
+ yield return nestedType;
+
+ foreach (var childType in EnumerateNestedTypes(nestedType))
+ yield return childType;
+ }
+ }
+
+ private static IEnumerable EnumerateBaseTypes(ITypeSymbol type)
+ {
+ for (var current = type.BaseType; current != null; current = current.BaseType)
+ yield return current;
+ }
+
+ private static bool HasCompatibleSignature(
+ IMethodSymbol candidate,
+ IMethodSymbol target)
+ {
+ if (candidate.Parameters.Length != target.Parameters.Length)
+ return false;
+
+ if (candidate.TypeParameters.Length != target.TypeParameters.Length)
+ return false;
+
+ return true;
+ }
+ }
+}
diff --git a/GFramework.SourceGenerators/Diagnostics/ContextRegistrationDiagnostics.cs b/GFramework.SourceGenerators/Diagnostics/ContextRegistrationDiagnostics.cs
new file mode 100644
index 00000000..7643c812
--- /dev/null
+++ b/GFramework.SourceGenerators/Diagnostics/ContextRegistrationDiagnostics.cs
@@ -0,0 +1,44 @@
+using GFramework.SourceGenerators.Common.Constants;
+
+namespace GFramework.SourceGenerators.Diagnostics;
+
+///
+/// 提供 Context Get 注册可见性分析相关诊断。
+///
+public static class ContextRegistrationDiagnostics
+{
+ private const string SourceGeneratorsRuleCategory = $"{PathContests.SourceGeneratorsPath}.Rule";
+
+ ///
+ /// 当模型使用点在所属架构中找不到静态可见注册时报告。
+ ///
+ public static readonly DiagnosticDescriptor ModelRegistrationMissing = new(
+ "GF_ContextRegistration_001",
+ "Model usage has no statically discoverable registration",
+ "Model '{0}' used by '{1}' is not statically registered in architecture '{2}'",
+ SourceGeneratorsRuleCategory,
+ DiagnosticSeverity.Warning,
+ true);
+
+ ///
+ /// 当系统使用点在所属架构中找不到静态可见注册时报告。
+ ///
+ public static readonly DiagnosticDescriptor SystemRegistrationMissing = new(
+ "GF_ContextRegistration_002",
+ "System usage has no statically discoverable registration",
+ "System '{0}' used by '{1}' is not statically registered in architecture '{2}'",
+ SourceGeneratorsRuleCategory,
+ DiagnosticSeverity.Warning,
+ true);
+
+ ///
+ /// 当工具使用点在所属架构中找不到静态可见注册时报告。
+ ///
+ public static readonly DiagnosticDescriptor UtilityRegistrationMissing = new(
+ "GF_ContextRegistration_003",
+ "Utility usage has no statically discoverable registration",
+ "Utility '{0}' used by '{1}' is not statically registered in architecture '{2}'",
+ SourceGeneratorsRuleCategory,
+ DiagnosticSeverity.Warning,
+ true);
+}
diff --git a/GFramework.SourceGenerators/README.md b/GFramework.SourceGenerators/README.md
index 7ed6db83..0d60714c 100644
--- a/GFramework.SourceGenerators/README.md
+++ b/GFramework.SourceGenerators/README.md
@@ -50,3 +50,14 @@ public partial class InventoryPanel
`[GetAll]` 会跳过 `const`、`static` 和 `readonly` 字段。若某个字段本来会被 `[GetAll]` 推断为
`Model`、`System` 或 `Utility` 绑定,但因为是不可赋值的 `static` 或 `readonly` 字段而被跳过,生成器会发出警告提示该字段不会参与生成。
+
+## 注册分析器
+
+包现在同时包含一个注册可见性分析器,用于检查 `Model`、`System`、`Utility` 的使用点是否能在所属架构中找到静态可见注册。
+
+- 覆盖字段特性注入:`[GetModel]`、`[GetModels]`、`[GetSystem]`、`[GetSystems]`、`[GetUtility]`、`[GetUtilities]`
+- 覆盖手写调用:`GetModel()`、`GetModels()`、`GetSystem()`、`GetSystems()`、`GetUtility()`、`GetUtilities()`
+- 默认报告 `Warning`
+- 当前只分析静态可见的注册路径,例如 `OnInitialize()`、`InstallModules()`、`InstallModule(new Module())`
+
+对于反射、运行时条件分支、外部程序集动态注册等路径,分析器不会强行推断;当无法唯一确定组件所属架构时,也会选择不报,优先降低误报。
diff --git a/docs/zh-CN/source-generators/context-get-generator.md b/docs/zh-CN/source-generators/context-get-generator.md
index d83fc394..1d4aea43 100644
--- a/docs/zh-CN/source-generators/context-get-generator.md
+++ b/docs/zh-CN/source-generators/context-get-generator.md
@@ -621,6 +621,33 @@ public partial class GameController
如果构造函数执行时上下文尚未建立,过早注入会失败;即使在构造函数中调用了注入方法,也不要在调用之前访问这些字段。
+## 注册可见性分析
+
+除了生成注入方法,`GFramework.SourceGenerators` 现在还会分析 `Model`、`System`、`Utility` 的使用点是否存在静态可见注册。
+
+当前支持的使用点:
+
+- 字段特性注入:`[GetModel]`、`[GetModels]`、`[GetSystem]`、`[GetSystems]`、`[GetUtility]`、`[GetUtilities]`
+- 手写调用:`this.GetModel()`、`this.GetSystem()`、`this.GetUtility()`
+
+当前支持的注册来源:
+
+- `Architecture.OnInitialize()` 中的直接 `RegisterModel/RegisterSystem/RegisterUtility`
+- `InstallModules()` 中的直接注册
+- `InstallModule(new SomeModule())` 和 `InstallGodotModule(new SomeModule())` 可静态展开到 `module.Install(...)` 的直接注册
+
+分析器会在能够唯一定位所属架构时,对缺失注册报告 `Warning`:
+
+- `GF_ContextRegistration_001`:`Model` 使用点未找到静态可见注册
+- `GF_ContextRegistration_002`:`System` 使用点未找到静态可见注册
+- `GF_ContextRegistration_003`:`Utility` 使用点未找到静态可见注册
+
+这个分析器的目标是提供稳定提示,而不是完整模拟运行时 DI 图。以下场景默认不做强推断:
+
+- 运行时条件分支控制的注册
+- 反射、配置驱动或外部程序集动态注册
+- 无法唯一判定组件归属架构的多架构场景
+
## 高级场景
### 泛型类型支持
diff --git a/docs/zh-CN/source-generators/index.md b/docs/zh-CN/source-generators/index.md
index b56d9ca2..7094a9e7 100644
--- a/docs/zh-CN/source-generators/index.md
+++ b/docs/zh-CN/source-generators/index.md
@@ -611,9 +611,12 @@ public enum ConflictEnum
| `GF_ContextGet_001` | 嵌套类不支持生成注入 | 将目标类型提取为顶层类 |
| `GF_ContextGet_002` | 注入字段不能为 `static` | 改为实例字段 |
| `GF_ContextGet_003` | 注入字段不能为 `readonly` | 移除 `readonly` |
-| `GF_ContextGet_004` | 字段类型与注入特性不匹配 | 使用符合特性约束的字段类型 |
-| `GF_ContextGet_005` | 目标类型必须具备上下文访问能力 | 添加 `[ContextAware]`、实现 `IContextAware` 或继承 `ContextAwareBase` |
-| `GF_ContextGet_006` | 同一字段不能声明多个注入特性 | 每个字段只保留一个注入特性 |
+| `GF_ContextGet_004` | 字段类型与注入特性不匹配 | 使用符合特性约束的字段类型 |
+| `GF_ContextGet_005` | 目标类型必须具备上下文访问能力 | 添加 `[ContextAware]`、实现 `IContextAware` 或继承 `ContextAwareBase` |
+| `GF_ContextGet_006` | 同一字段不能声明多个注入特性 | 每个字段只保留一个注入特性 |
+| `GF_ContextRegistration_001` | `Model` 使用点没有静态可见注册 | 在所属架构的初始化链路中显式注册对应 `Model` |
+| `GF_ContextRegistration_002` | `System` 使用点没有静态可见注册 | 在所属架构的初始化链路中显式注册对应 `System` |
+| `GF_ContextRegistration_003` | `Utility` 使用点没有静态可见注册 | 在所属架构的初始化链路中显式注册对应 `Utility` |
## 性能优势
From 8eaf83732732cab421c99f58d76a805d0a49a9ac Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 3 Apr 2026 23:50:09 +0800
Subject: [PATCH 2/3] =?UTF-8?q?refactor(analyzer):=20=E4=BC=98=E5=8C=96?=
=?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E6=B3=A8=E5=86=8C=E5=88=86=E6=9E=90?=
=?UTF-8?q?=E5=99=A8=E4=B8=AD=E7=9A=84=E8=AF=AD=E6=B3=95=E5=8C=B9=E9=85=8D?=
=?UTF-8?q?=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 使用属性模式匹配替换条件判断语句
- 简化了方法声明语法的空值检查逻辑
- 优化了构造函数声明语法的表达式体检查
- 提高了代码可读性和维护性
- 减少了冗余的语法树遍历操作
---
.../Analyzers/ContextRegistrationAnalyzer.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/GFramework.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs b/GFramework.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs
index 5d20f79e..c691fe65 100644
--- a/GFramework.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs
+++ b/GFramework.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs
@@ -818,13 +818,13 @@ public sealed class ContextRegistrationAnalyzer : DiagnosticAnalyzer
var semanticModel = compilation.GetSemanticModel(syntax.SyntaxTree);
var operation = syntax switch
{
- MethodDeclarationSyntax methodDeclaration when methodDeclaration.Body != null =>
+ MethodDeclarationSyntax { Body: not null } methodDeclaration =>
semanticModel.GetOperation(methodDeclaration.Body),
- MethodDeclarationSyntax methodDeclaration when methodDeclaration.ExpressionBody != null =>
+ MethodDeclarationSyntax { ExpressionBody: not null } methodDeclaration =>
semanticModel.GetOperation(methodDeclaration.ExpressionBody.Expression),
- ConstructorDeclarationSyntax constructorDeclaration when constructorDeclaration.Body != null =>
+ ConstructorDeclarationSyntax { Body: not null } constructorDeclaration =>
semanticModel.GetOperation(constructorDeclaration.Body),
- ConstructorDeclarationSyntax constructorDeclaration when constructorDeclaration.ExpressionBody != null =>
+ ConstructorDeclarationSyntax { ExpressionBody: not null } constructorDeclaration =>
semanticModel.GetOperation(constructorDeclaration.ExpressionBody.Expression),
_ => null
};
From 7901a6902848c06976f88907d9a02eb55d7fe1b7 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Sat, 4 Apr 2026 23:42:05 +0800
Subject: [PATCH 3/3] =?UTF-8?q?docs(generator):=20=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E8=8E=B7=E5=8F=96=E7=94=9F=E6=88=90?=
=?UTF-8?q?=E5=99=A8=E6=96=87=E6=A1=A3=E4=B8=AD=E7=9A=84=E6=8E=AA=E8=BE=9E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 将“强推断”更正为“强行推断”以提高表述准确性
---
docs/zh-CN/source-generators/context-get-generator.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/zh-CN/source-generators/context-get-generator.md b/docs/zh-CN/source-generators/context-get-generator.md
index 1d4aea43..831d20c6 100644
--- a/docs/zh-CN/source-generators/context-get-generator.md
+++ b/docs/zh-CN/source-generators/context-get-generator.md
@@ -642,7 +642,7 @@ public partial class GameController
- `GF_ContextRegistration_002`:`System` 使用点未找到静态可见注册
- `GF_ContextRegistration_003`:`Utility` 使用点未找到静态可见注册
-这个分析器的目标是提供稳定提示,而不是完整模拟运行时 DI 图。以下场景默认不做强推断:
+这个分析器的目标是提供稳定提示,而不是完整模拟运行时 DI 图。以下场景默认不做强行推断:
- 运行时条件分支控制的注册
- 反射、配置驱动或外部程序集动态注册