diff --git a/AGENTS.md b/AGENTS.md index c3932978..ae03504e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,9 +29,10 @@ All AI agents and contributors must follow these rules when writing, reviewing, ## Git Workflow Rules - Every completed task MUST pass at least one build validation before it is considered done. -- When the goal is to inspect or reduce warnings printed during project build, contributors MUST start from a plain - `dotnet build` at the repository root and treat that output as the default warning inspection entrypoint before - adding extra build parameters or switching to narrower commands. +- When the goal is to inspect or reduce warnings printed during project build, contributors MUST establish the warning + baseline from a non-incremental repository-root build by running `dotnet clean` and then `dotnet build`. +- Contributors MUST NOT treat a repeated incremental `dotnet build` result as authoritative for warning inspection when + a clean baseline has not been captured in the same round. - If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project `dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles. - When a task adds a feature or modifies code, contributors MUST run a Release build for every directly affected @@ -237,6 +238,7 @@ Use the smallest command set that proves the change, then expand if the change i ```bash # Check warnings from the default repository build entrypoint +dotnet clean dotnet build # Build the full solution diff --git a/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs index 610662a2..e0e4e3a1 100644 --- a/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs +++ b/GFramework.Godot.SourceGenerators/BindNodeSignalGenerator.cs @@ -72,19 +72,8 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator if (bindNodeSignalAttribute is null || godotNodeSymbol is null) return; - // 缓存每个方法上已解析的特性,避免在筛选和生成阶段重复做语义查询。 - var methodAttributes = candidates - .Where(static candidate => candidate is not null) - .Select(static candidate => candidate!) - .ToDictionary( - static candidate => candidate, - candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute), - ReferenceEqualityComparer.Instance); - - var methodCandidates = methodAttributes - .Where(static pair => pair.Value.Count > 0) - .Select(static pair => pair.Key) - .ToList(); + var methodAttributes = BuildMethodAttributeMap(candidates, bindNodeSignalAttribute); + var methodCandidates = CollectMethodCandidates(methodAttributes); foreach (var group in GroupByContainingType(methodCandidates)) { @@ -99,19 +88,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator UnbindMethodName)) continue; - var bindings = new List(); - - foreach (var candidate in group.Methods) - { - foreach (var attribute in methodAttributes[candidate]) - { - if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding)) - continue; - - bindings.Add(binding); - } - } - + var bindings = CollectBindings(context, group, methodAttributes, godotNodeSymbol); if (bindings.Count == 0) continue; @@ -171,99 +148,22 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator if (candidate.MethodSymbol.IsStatic) { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.StaticMethodNotSupported, - candidate, - attribute, - candidate.MethodSymbol.Name); + ReportStaticMethodDiagnostic(context, candidate, attribute); return false; } - if (!TryResolveCtorString(attribute, 0, out var nodeFieldName)) - { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.InvalidConstructorArgument, - candidate, - attribute, - candidate.MethodSymbol.Name, - "nodeFieldName"); + if (!TryResolveBindingTargetNames(context, candidate, attribute, out var nodeFieldName, out var signalName)) return false; - } - if (!TryResolveCtorString(attribute, 1, out var signalName)) - { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.InvalidConstructorArgument, - candidate, - attribute, - candidate.MethodSymbol.Name, - "signalName"); + if (!TryFindCompatibleField(context, candidate, attribute, godotNodeSymbol, nodeFieldName, out var fieldSymbol)) return false; - } - var fieldSymbol = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName); - if (fieldSymbol is null) - { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.NodeFieldNotFound, - candidate, - attribute, - candidate.MethodSymbol.Name, - nodeFieldName, - candidate.MethodSymbol.ContainingType.Name); + if (!TryFindCompatibleEvent(context, candidate, attribute, fieldSymbol, signalName, out var eventSymbol)) return false; - } - - if (fieldSymbol.IsStatic) - { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField, - candidate, - attribute, - candidate.MethodSymbol.Name, - fieldSymbol.Name); - return false; - } - - if (!fieldSymbol.Type.IsAssignableTo(godotNodeSymbol)) - { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode, - candidate, - attribute, - fieldSymbol.Name); - return false; - } - - var eventSymbol = FindEvent(fieldSymbol.Type, signalName); - if (eventSymbol is null) - { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.SignalNotFound, - candidate, - attribute, - fieldSymbol.Name, - signalName); - return false; - } if (!IsMethodCompatibleWithEvent(candidate.MethodSymbol, eventSymbol)) { - ReportMethodDiagnostic( - context, - BindNodeSignalDiagnostics.MethodSignatureNotCompatible, - candidate, - attribute, - candidate.MethodSymbol.Name, - eventSymbol.Name, - fieldSymbol.Name); + ReportIncompatibleSignatureDiagnostic(context, candidate, attribute, eventSymbol, fieldSymbol); return false; } @@ -271,6 +171,235 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator return true; } + private static Dictionary> BuildMethodAttributeMap( + ImmutableArray candidates, + INamedTypeSymbol bindNodeSignalAttribute) + { + return candidates + .Where(static candidate => candidate is not null) + .Select(static candidate => candidate!) + .ToDictionary( + static candidate => candidate, + candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute), + ReferenceEqualityComparer.Instance); + } + + private static List CollectMethodCandidates( + IReadOnlyDictionary> methodAttributes) + { + return methodAttributes + .Where(static pair => pair.Value.Count > 0) + .Select(static pair => pair.Key) + .ToList(); + } + + private static List CollectBindings( + SourceProductionContext context, + TypeGroup group, + IReadOnlyDictionary> methodAttributes, + INamedTypeSymbol godotNodeSymbol) + { + var bindings = new List(); + + foreach (var candidate in group.Methods) + { + foreach (var attribute in methodAttributes[candidate]) + { + if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding)) + continue; + + bindings.Add(binding); + } + } + + return bindings; + } + + private static void ReportStaticMethodDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.StaticMethodNotSupported, + candidate, + attribute, + candidate.MethodSymbol.Name); + } + + private static bool TryResolveBindingTargetNames( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + out string nodeFieldName, + out string signalName) + { + nodeFieldName = string.Empty; + signalName = string.Empty; + + if (!TryResolveCtorString(attribute, 0, out nodeFieldName)) + { + ReportInvalidConstructorArgumentDiagnostic(context, candidate, attribute, "nodeFieldName"); + return false; + } + + if (!TryResolveCtorString(attribute, 1, out signalName)) + { + ReportInvalidConstructorArgumentDiagnostic(context, candidate, attribute, "signalName"); + return false; + } + + return true; + } + + private static void ReportInvalidConstructorArgumentDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + string argumentName) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.InvalidConstructorArgument, + candidate, + attribute, + candidate.MethodSymbol.Name, + argumentName); + } + + private static bool TryFindCompatibleField( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + INamedTypeSymbol godotNodeSymbol, + string nodeFieldName, + out IFieldSymbol fieldSymbol) + { + fieldSymbol = null!; + + var resolvedField = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName); + if (resolvedField is null) + { + ReportNodeFieldNotFoundDiagnostic(context, candidate, attribute, nodeFieldName); + return false; + } + + if (resolvedField.IsStatic) + { + ReportNodeFieldMustBeInstanceDiagnostic(context, candidate, attribute, resolvedField); + return false; + } + + if (!resolvedField.Type.IsAssignableTo(godotNodeSymbol)) + { + ReportFieldTypeMustDeriveFromNodeDiagnostic(context, candidate, attribute, resolvedField); + return false; + } + + fieldSymbol = resolvedField; + return true; + } + + private static void ReportNodeFieldNotFoundDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + string nodeFieldName) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.NodeFieldNotFound, + candidate, + attribute, + candidate.MethodSymbol.Name, + nodeFieldName, + candidate.MethodSymbol.ContainingType.Name); + } + + private static void ReportNodeFieldMustBeInstanceDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + IFieldSymbol fieldSymbol) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField, + candidate, + attribute, + candidate.MethodSymbol.Name, + fieldSymbol.Name); + } + + private static void ReportFieldTypeMustDeriveFromNodeDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + IFieldSymbol fieldSymbol) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode, + candidate, + attribute, + fieldSymbol.Name); + } + + private static bool TryFindCompatibleEvent( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + IFieldSymbol fieldSymbol, + string signalName, + out IEventSymbol eventSymbol) + { + eventSymbol = null!; + + var resolvedEvent = FindEvent(fieldSymbol.Type, signalName); + if (resolvedEvent is null) + { + ReportSignalNotFoundDiagnostic(context, candidate, attribute, fieldSymbol, signalName); + return false; + } + + eventSymbol = resolvedEvent; + return true; + } + + private static void ReportSignalNotFoundDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + IFieldSymbol fieldSymbol, + string signalName) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.SignalNotFound, + candidate, + attribute, + fieldSymbol.Name, + signalName); + } + + private static void ReportIncompatibleSignatureDiagnostic( + SourceProductionContext context, + MethodCandidate candidate, + AttributeData attribute, + IEventSymbol eventSymbol, + IFieldSymbol fieldSymbol) + { + ReportMethodDiagnostic( + context, + BindNodeSignalDiagnostics.MethodSignatureNotCompatible, + candidate, + attribute, + candidate.MethodSymbol.Name, + eventSymbol.Name, + fieldSymbol.Name); + } + private static void ReportMethodDiagnostic( SourceProductionContext context, DiagnosticDescriptor descriptor, @@ -404,11 +533,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator { return typeSymbol.GetMembers() .OfType() - .FirstOrDefault(method => - method.Name == methodName && - !method.IsStatic && - method.Parameters.Length == 0 && - method.MethodKind == MethodKind.Ordinary); + .FirstOrDefault(method => IsParameterlessInstanceMethod(method, methodName)); } private static bool CallsGeneratedMethod( @@ -447,6 +572,14 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator }; } + private static bool IsParameterlessInstanceMethod(IMethodSymbol method, string methodName) + { + return string.Equals(method.Name, methodName, StringComparison.Ordinal) && + !method.IsStatic && + method.Parameters.Length == 0 && + method.MethodKind == MethodKind.Ordinary; + } + private static bool IsBindNodeSignalAttributeName(NameSyntax attributeName) { var simpleName = GetAttributeSimpleName(attributeName); @@ -608,4 +741,4 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator return RuntimeHelpers.GetHashCode(obj); } } -} \ No newline at end of file +} diff --git a/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs b/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs index 6aa3ac81..7122faab 100644 --- a/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs +++ b/GFramework.Godot.SourceGenerators/GetNodeGenerator.cs @@ -259,11 +259,7 @@ public sealed class GetNodeGenerator : IIncrementalGenerator { return typeSymbol.GetMembers() .OfType() - .FirstOrDefault(static method => - method.Name == "_Ready" && - !method.IsStatic && - method.Parameters.Length == 0 && - method.MethodKind == MethodKind.Ordinary); + .FirstOrDefault(static method => IsParameterlessInstanceMethod(method, "_Ready")); } private static bool CallsGeneratedInjection(IMethodSymbol readyMethod) @@ -306,6 +302,14 @@ public sealed class GetNodeGenerator : IIncrementalGenerator return attribute.GetNamedArgument("Required", true); } + private static bool IsParameterlessInstanceMethod(IMethodSymbol method, string methodName) + { + return string.Equals(method.Name, methodName, StringComparison.Ordinal) && + !method.IsStatic && + method.Parameters.Length == 0 && + method.MethodKind == MethodKind.Ordinary; + } + private static bool TryResolvePath( IFieldSymbol fieldSymbol, AttributeData attribute, @@ -373,7 +377,10 @@ public sealed class GetNodeGenerator : IIncrementalGenerator if (!string.Equals(namedArgument.Key, "Lookup", StringComparison.Ordinal)) continue; - if (namedArgument.Value.Type?.ToDisplayString() != GetNodeLookupModeMetadataName) + if (!string.Equals( + namedArgument.Value.Type?.ToDisplayString(), + GetNodeLookupModeMetadataName, + StringComparison.Ordinal)) continue; if (namedArgument.Value.Value is int value) @@ -568,4 +575,4 @@ public sealed class GetNodeGenerator : IIncrementalGenerator public List Fields { get; } = new(); } -} \ No newline at end of file +} diff --git a/GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs b/GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs index c080cc71..400c2f31 100644 --- a/GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs +++ b/GFramework.Godot.SourceGenerators/GodotProjectMetadataGenerator.cs @@ -126,7 +126,27 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator var explicitMappings = new Dictionary>(StringComparer.Ordinal); var implicitCandidates = new Dictionary>(StringComparer.Ordinal); + CollectMappingCandidates( + context, + typeCandidates, + autoLoadAttributeSymbol, + godotNodeSymbol, + projectAutoLoadNames, + explicitMappings, + implicitCandidates); + return ResolveTypedMappings(context, projectAutoLoadNames, explicitMappings, implicitCandidates); + } + + private static void CollectMappingCandidates( + SourceProductionContext context, + IReadOnlyList typeCandidates, + INamedTypeSymbol? autoLoadAttributeSymbol, + INamedTypeSymbol godotNodeSymbol, + ISet projectAutoLoadNames, + IDictionary> explicitMappings, + IDictionary> implicitCandidates) + { foreach (var candidate in typeCandidates) { var typeSymbol = candidate.TypeSymbol; @@ -176,7 +196,14 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator explicitList.Add(typeSymbol); } + } + private static Dictionary ResolveTypedMappings( + SourceProductionContext context, + IEnumerable projectAutoLoadNames, + IReadOnlyDictionary> explicitMappings, + IReadOnlyDictionary> implicitCandidates) + { var resolvedMappings = new Dictionary(StringComparer.Ordinal); foreach (var projectAutoLoadName in projectAutoLoadNames.OrderBy(static name => name, StringComparer.Ordinal)) @@ -408,24 +435,40 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator foreach (var member in members) { - builder.AppendLine(" /// "); - builder.AppendLine($" /// 获取 AutoLoad {member.AutoLoadName}。"); - builder.AppendLine(" /// "); - builder.AppendLine( - $" public static {member.TypeName} {member.Identifier} => GetRequiredNode<{member.TypeName}>({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)});"); - builder.AppendLine(); - builder.AppendLine(" /// "); - builder.AppendLine($" /// 尝试获取 AutoLoad {member.AutoLoadName}。"); - builder.AppendLine(" /// "); - builder.AppendLine( - $" public static bool TryGet{member.Identifier}(out {member.TypeName}? value)"); - builder.AppendLine(" {"); - builder.AppendLine( - $" return TryGetNode({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)}, out value);"); - builder.AppendLine(" }"); - builder.AppendLine(); + AppendAutoLoadMemberSource(builder, member); } + AppendGetRequiredNodeSource(builder); + AppendTryGetNodeSource(builder); + builder.AppendLine("}"); + + return builder.ToString(); + } + + private static void AppendAutoLoadMemberSource( + StringBuilder builder, + GeneratedAutoLoadMember member) + { + builder.AppendLine(" /// "); + builder.AppendLine($" /// 获取 AutoLoad {member.AutoLoadName}。"); + builder.AppendLine(" /// "); + builder.AppendLine( + $" public static {member.TypeName} {member.Identifier} => GetRequiredNode<{member.TypeName}>({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)});"); + builder.AppendLine(); + builder.AppendLine(" /// "); + builder.AppendLine($" /// 尝试获取 AutoLoad {member.AutoLoadName}。"); + builder.AppendLine(" /// "); + builder.AppendLine( + $" public static bool TryGet{member.Identifier}(out {member.TypeName}? value)"); + builder.AppendLine(" {"); + builder.AppendLine( + $" return TryGetNode({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)}, out value);"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + + private static void AppendGetRequiredNodeSource(StringBuilder builder) + { builder.AppendLine(" /// "); builder.AppendLine(" /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。"); builder.AppendLine(" /// "); @@ -444,6 +487,10 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator " throw new global::System.InvalidOperationException($\"AutoLoad '{autoLoadName}' is not available on the active SceneTree root.\");"); builder.AppendLine(" }"); builder.AppendLine(); + } + + private static void AppendTryGetNodeSource(StringBuilder builder) + { builder.AppendLine(" /// "); builder.AppendLine(" /// 尝试从当前 SceneTree 根节点解析 AutoLoad。"); builder.AppendLine(" /// "); @@ -470,9 +517,6 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator builder.AppendLine(" value = root.GetNodeOrNull($\"/root/{autoLoadName}\");"); builder.AppendLine(" return value is not null;"); builder.AppendLine(" }"); - builder.AppendLine("}"); - - return builder.ToString(); } private static string GenerateInputActionsSource(IReadOnlyList members) @@ -530,45 +574,16 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator if (string.IsNullOrWhiteSpace(content) || content.StartsWith(";", StringComparison.Ordinal)) continue; - if (content.StartsWith("[", StringComparison.Ordinal) && content.EndsWith("]", StringComparison.Ordinal)) - { - currentSection = content.Substring(1, content.Length - 2).Trim(); + if (TryUpdateSection(content, ref currentSection)) continue; - } if (!TryParseAssignment(content, out var key, out var value)) continue; - if (string.Equals(currentSection, "autoload", StringComparison.OrdinalIgnoreCase)) - { - if (!seenAutoLoads.Add(key)) - { - diagnostics.Add(Diagnostic.Create( - GodotProjectDiagnostics.DuplicateAutoLoadEntry, - CreateFileLocation(file.Path), - key)); - continue; - } - - autoLoads.Add(new ProjectAutoLoadEntry( - key, - NormalizeProjectPath(value))); + if (TryCollectAutoLoadEntry(file, currentSection, key, value, seenAutoLoads, autoLoads, diagnostics)) continue; - } - if (string.Equals(currentSection, "input", StringComparison.OrdinalIgnoreCase)) - { - if (!seenInputActions.Add(key)) - { - diagnostics.Add(Diagnostic.Create( - GodotProjectDiagnostics.DuplicateInputActionEntry, - CreateFileLocation(file.Path), - key)); - continue; - } - - inputActions.Add(key); - } + TryCollectInputAction(currentSection, key, seenInputActions, inputActions, diagnostics, file.Path); } return new ProjectMetadataParseResult( @@ -578,6 +593,68 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator diagnostics.ToImmutableArray()); } + private static bool TryUpdateSection(string content, ref string currentSection) + { + if (!content.StartsWith("[", StringComparison.Ordinal) || + !content.EndsWith("]", StringComparison.Ordinal)) + { + return false; + } + + currentSection = content.Substring(1, content.Length - 2).Trim(); + return true; + } + + private static bool TryCollectAutoLoadEntry( + AdditionalText file, + string currentSection, + string key, + string value, + ISet seenAutoLoads, + ICollection autoLoads, + ICollection diagnostics) + { + if (!string.Equals(currentSection, "autoload", StringComparison.OrdinalIgnoreCase)) + return false; + + if (!seenAutoLoads.Add(key)) + { + diagnostics.Add(Diagnostic.Create( + GodotProjectDiagnostics.DuplicateAutoLoadEntry, + CreateFileLocation(file.Path), + key)); + return true; + } + + autoLoads.Add(new ProjectAutoLoadEntry( + key, + NormalizeProjectPath(value))); + return true; + } + + private static void TryCollectInputAction( + string currentSection, + string key, + ISet seenInputActions, + ICollection inputActions, + ICollection diagnostics, + string filePath) + { + if (!string.Equals(currentSection, "input", StringComparison.OrdinalIgnoreCase)) + return; + + if (!seenInputActions.Add(key)) + { + diagnostics.Add(Diagnostic.Create( + GodotProjectDiagnostics.DuplicateInputActionEntry, + CreateFileLocation(filePath), + key)); + return; + } + + inputActions.Add(key); + } + private static string NormalizeProjectPath(string rawValue) { var trimmed = rawValue.Trim(); diff --git a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs index 93219dcb..23dc1109 100644 --- a/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs +++ b/GFramework.Godot.SourceGenerators/Registration/AutoRegisterExportedCollectionsGenerator.cs @@ -190,6 +190,48 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener { registration = null!; + if (!TryResolveCollectionType(context, collectionMember, enumerableType, out var collectionType)) + return false; + + if (!TryResolveRegistryTarget( + context, + compilation, + ownerType, + collectionMember, + attribute, + out var registryMemberName, + out var registerMethodName, + out var registryType)) + { + return false; + } + + if (!TryResolveElementType(context, collectionMember, collectionType, out var elementType)) + return false; + + if (!HasCompatibleRegisterMethod(compilation, ownerType, registryType, registerMethodName, elementType)) + { + context.ReportDiagnostic(Diagnostic.Create( + AutoRegisterExportedCollectionsDiagnostics.RegisterMethodNotFound, + collectionMember.Locations.FirstOrDefault() ?? Location.None, + registerMethodName, + registryMemberName, + collectionMember.Name)); + return false; + } + + registration = new RegistrationSpec(collectionMember.Name, registryMemberName, registerMethodName); + return true; + } + + private static bool TryResolveCollectionType( + SourceProductionContext context, + ISymbol collectionMember, + INamedTypeSymbol enumerableType, + out ITypeSymbol collectionType) + { + collectionType = null!; + if (!IsInstanceReadableMember(collectionMember)) { context.ReportDiagnostic(Diagnostic.Create( @@ -199,17 +241,11 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return false; } - var collectionType = collectionMember switch - { - IFieldSymbol field => field.Type, - IPropertySymbol property => property.Type, - _ => null - }; - - if (collectionType is null) + var resolvedType = GetMemberType(collectionMember); + if (resolvedType is null) return false; - if (!collectionType.IsAssignableTo(enumerableType)) + if (!resolvedType.IsAssignableTo(enumerableType)) { context.ReportDiagnostic(Diagnostic.Create( AutoRegisterExportedCollectionsDiagnostics.CollectionTypeMustBeEnumerable, @@ -218,12 +254,35 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return false; } - if (!TryGetRegistrationAttributeArguments(context, collectionMember, attribute, out var registryMemberName, - out var registerMethodName)) + collectionType = resolvedType; + return true; + } + + private static bool TryResolveRegistryTarget( + SourceProductionContext context, + Compilation compilation, + INamedTypeSymbol ownerType, + ISymbol collectionMember, + AttributeData attribute, + out string registryMemberName, + out string registerMethodName, + out INamedTypeSymbol registryType) + { + registryMemberName = string.Empty; + registerMethodName = string.Empty; + registryType = null!; + + if (!TryGetRegistrationAttributeArguments( + context, + collectionMember, + attribute, + out registryMemberName, + out registerMethodName)) + { return false; + } var registryMember = FindRegistryMember(ownerType, registryMemberName); - if (registryMember is null) { context.ReportDiagnostic(Diagnostic.Create( @@ -246,18 +305,24 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return false; } - var registryType = registryMember switch - { - IFieldSymbol field => field.Type as INamedTypeSymbol, - IPropertySymbol property => property.Type as INamedTypeSymbol, - _ => null - }; - - if (registryType is null) + var resolvedRegistryType = GetMemberType(registryMember) as INamedTypeSymbol; + if (resolvedRegistryType is null) return false; - var elementType = TryGetElementType(collectionType); - if (elementType is null) + registryType = resolvedRegistryType; + return true; + } + + private static bool TryResolveElementType( + SourceProductionContext context, + ISymbol collectionMember, + ITypeSymbol collectionType, + out ITypeSymbol elementType) + { + elementType = null!; + + var resolvedElementType = TryGetElementType(collectionType); + if (resolvedElementType is null) { // Non-generic IEnumerable exposes elements as object at compile time, which is not safe // for validating or generating a strongly typed registry call. @@ -268,26 +333,33 @@ public sealed class AutoRegisterExportedCollectionsGenerator : IIncrementalGener return false; } - var hasCompatibleMethod = EnumerateCandidateMethods(registryType, registerMethodName) + elementType = resolvedElementType; + return true; + } + + private static bool HasCompatibleRegisterMethod( + Compilation compilation, + INamedTypeSymbol ownerType, + INamedTypeSymbol registryType, + string registerMethodName, + ITypeSymbol elementType) + { + return EnumerateCandidateMethods(registryType, registerMethodName) .Any(method => !method.IsStatic && method.Parameters.Length == 1 && compilation.IsSymbolAccessibleWithin(method, ownerType) && CanAcceptElementType(compilation, elementType, method.Parameters[0].Type)); + } - if (!hasCompatibleMethod) + private static ITypeSymbol? GetMemberType(ISymbol member) + { + return member switch { - context.ReportDiagnostic(Diagnostic.Create( - AutoRegisterExportedCollectionsDiagnostics.RegisterMethodNotFound, - collectionMember.Locations.FirstOrDefault() ?? Location.None, - registerMethodName, - registryMemberName, - collectionMember.Name)); - return false; - } - - registration = new RegistrationSpec(collectionMember.Name, registryMemberName, registerMethodName); - return true; + IFieldSymbol field => field.Type, + IPropertySymbol property => property.Type, + _ => null + }; } private static bool IsInstanceReadableMember(ISymbol member) diff --git a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md index 22fffe03..dbc281af 100644 --- a/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md +++ b/ai-plan/public/analyzer-warning-reduction/todos/analyzer-warning-reduction-tracking.md @@ -6,26 +6,27 @@ ## 当前恢复点 -- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-049` -- 当前阶段:`Phase 49` +- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-050` +- 当前阶段:`Phase 50` - 当前焦点: - - 默认 warning 检查入口已统一为仓库根目录直接执行 `dotnet build` - - `2026-04-24` 最新一次 plain `dotnet build` 结果为 `Build succeeded.`、`0 Warning(s)`、`0 Error(s)` - - 当前分支仍为 `fix/analyzer-warning-reduction-batch`,最近相关提交包括 `77e332f` 与 `a98d1cb` - - 当前工作树除未跟踪的 `.codex` 目录外无活动代码修改 + - warning 基线已修正为仓库根目录执行 `dotnet clean` 后再执行 `dotnet build` + - `2026-04-24` 用户确认的 clean solution build 结果为 `Build succeeded with 1193 warning(s)` + - 当前主线程切片为 `GFramework.Godot.SourceGenerators` + - 当前工作树除未跟踪的 `.codex` 目录外,存在待提交的 source generator / `AGENTS.md` / `ai-plan` 修改 ## 当前活跃事实 -- 需要修复的对象是 plain `dotnet build` 实际打印出来的 warning,而不是不同 logger / 参数组合下的命令行为差异 -- 截至当前恢复点,默认 solution 构建入口没有打印 warning,因此没有可立即切分的 warning-fix 代码切片 -- `UnifiedSettingsFile`、`UnifiedSettingsDataRepository`、`LocalizationMap` 与 `CqrsHandlerRegistryGeneratorTests` 的上一轮 warning-reduction 修改已经提交在当前分支历史中 +- 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值 +- 本轮已完成 `GFramework.Godot.SourceGenerators` warning 清理:clean `Release` build 从 9 个 warning 降至 0 个 warning +- 当前已确认解决的文件包括 `BindNodeSignalGenerator.cs`、`GetNodeGenerator.cs`、`GodotProjectMetadataGenerator.cs`、`Registration/AutoRegisterExportedCollectionsGenerator.cs` +- 后续 warning-reduction 仍应以 clean solution build 的真实输出为切片来源 ## 当前风险 -- active 文档此前过度记录了 batch 停点、构建参数与旧 baseline 细节,容易把恢复重点带偏到“如何检查 warning”而不是“修 warning 本身” - - 缓解措施:active 文档只保留 plain `dotnet build` 的最新结果与下一步动作,把被替换的细节移入 archive -- 如果后续代码修改重新引入 warning,但没有先从 plain `dotnet build` 输出确认,就容易再次偏离当前分支目标 - - 缓解措施:后续每一轮都先跑 plain `dotnet build`,再按实际打印的 warning 逐项处理 +- 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0 + - 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build` +- 当前只验证了受影响项目 `GFramework.Godot.SourceGenerators`;整仓库 warning 总量仍应以用户确认的 clean solution build 为基线 + - 缓解措施:下一轮从 clean solution build 输出里选择新的低风险 warning 热点继续切片 ## 活跃文档 @@ -41,10 +42,14 @@ ## 验证说明 +- `dotnet clean GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` +- `dotnet build GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` - `dotnet build` - - 结果:成功;`0 Warning(s)`、`0 Error(s)`、`Time Elapsed 00:00:14.97` + - 结果:此前被误记为 `0 Warning(s)`;现已确认这是增量构建假阴性,不再作为有效基线 ## 下一步建议 -1. 后续继续当前分支目标时,先跑 plain `dotnet build`,只处理它实际打印出来的 warning -2. 如果下一轮 plain `dotnet build` 仍然保持 `0 Warning(s)`,则当前分支的 build-warning 目标可视为已完成 +1. 在仓库根目录先执行 `dotnet clean`、再执行 `dotnet build`,重新采集当前 solution 的真实 warning 列表 +2. 以 clean build 输出中的下一个低风险热点作为新切片,优先继续 source generator、测试或单模块可局部验证的问题 diff --git a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md index ced6be30..725b2f08 100644 --- a/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md +++ b/ai-plan/public/analyzer-warning-reduction/traces/analyzer-warning-reduction-trace.md @@ -1,22 +1,27 @@ # Analyzer Warning Reduction 追踪 -## 2026-04-24 — RP-049 +## 2026-04-24 — RP-050 -### 阶段:plain `dotnet build` 入口固化与 active 文档归档压缩 +### 阶段:clean-build 基线修正与 `GFramework.Godot.SourceGenerators` 切片清零 - 触发背景: - - 用户要求把“执行 `dotnet build` 来检查警告”写入 `AGENTS.md` - - 用户要求清理或归档 `analyzer-warning-reduction` 的 active todo / trace 内容 - - 用户明确要求继续当前分支的真实目标:修复项目构建时打印的 warning,而不是继续纠结 warning 检查命令本身 + - 用户确认之前的 `0 Warning(s)` 来自增量构建假阴性;只有先 `dotnet clean` 再 `dotnet build`,warning 才会重新出现 + - 用户给出 clean solution build 的真实结果:`Build succeeded with 1193 warning(s)` - 主线程实施: - - 直接在仓库根目录执行 plain `dotnet build` - - 构建结果为 `Build succeeded.`、`0 Warning(s)`、`0 Error(s)`、`Time Elapsed 00:00:14.97` - - 更新 `AGENTS.md`,明确 plain `dotnet build` 是当前仓库默认的 build-warning 检查入口 - - 将 RP-048 之前 active 文档中关于旧 baseline、batch 停点与构建参数形态的细节移入新的 archive 文件 - - 重写 active todo / trace,只保留当前恢复点需要的真值 + - 纠正当前 topic 的 active todo / trace,把 clean build 作为新的 warning 检查真值 + - 在 `BindNodeSignalGenerator.cs`、`GetNodeGenerator.cs`、`GodotProjectMetadataGenerator.cs` 中完成分阶段方法抽取与字符串比较修正 + - 在 `Registration/AutoRegisterExportedCollectionsGenerator.cs` 中拆分 `TryCreateRegistration`,清除最后一个 `MA0051` + - 更新 `AGENTS.md`,明确 warning 检查必须先 `dotnet clean` 再 `dotnet build` +- 验证里程碑: + - `dotnet clean GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release` + - 结果:成功;`0 Warning(s)`、`0 Error(s)` + - `dotnet build GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release` + - 首次验证:成功;`1 Warning(s)`,剩余 `Registration/AutoRegisterExportedCollectionsGenerator.cs(182,25)` `MA0051` + - 修复后复验:成功;`0 Warning(s)`、`0 Error(s)` - 当前结论: - - 当前分支在默认 solution 构建入口下没有打印 warning,因此此刻没有新的 warning-fix 代码切片可继续实施 - - 当前分支目标没有改变:后续只要 plain `dotnet build` 再次打印 warning,就以该输出为唯一切片来源继续修复 + - `GFramework.Godot.SourceGenerators` 已在 clean `Release` build 下从 9 个 warning 降到 0 个 warning + - 整仓库 warning 基线仍以用户确认的 clean solution build `1193 warning(s)` 为准 + - 下一轮应继续从 clean solution build 输出中选择新的低风险热点 ## Archive Context