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..c691fe65 --- /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 { Body: not null } methodDeclaration => + semanticModel.GetOperation(methodDeclaration.Body), + MethodDeclarationSyntax { ExpressionBody: not null } methodDeclaration => + semanticModel.GetOperation(methodDeclaration.ExpressionBody.Expression), + ConstructorDeclarationSyntax { Body: not null } constructorDeclaration => + semanticModel.GetOperation(constructorDeclaration.Body), + ConstructorDeclarationSyntax { ExpressionBody: not null } constructorDeclaration => + 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..831d20c6 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` | ## 性能优势