mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
refactor(godot-source-generators): 清理生成器告警与构建基线
- 重构 Godot source generator 的长方法与字符串比较逻辑,清理 GFramework.Godot.SourceGenerators 的 MA0051 和 MA0006 告警 - 更新 AutoRegisterExportedCollectionsGenerator 的注册解析阶段拆分,消除剩余的长方法告警 - 更新 AGENTS 与 analyzer-warning-reduction 跟踪文档,明确 warning 检查必须先 clean 再 build
This commit is contained in:
parent
25d33d0bf9
commit
a439fb8f4e
@ -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
|
||||
|
||||
@ -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<SignalBindingInfo>();
|
||||
|
||||
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<MethodCandidate, IReadOnlyList<AttributeData>> BuildMethodAttributeMap(
|
||||
ImmutableArray<MethodCandidate?> 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<MethodCandidate> CollectMethodCandidates(
|
||||
IReadOnlyDictionary<MethodCandidate, IReadOnlyList<AttributeData>> methodAttributes)
|
||||
{
|
||||
return methodAttributes
|
||||
.Where(static pair => pair.Value.Count > 0)
|
||||
.Select(static pair => pair.Key)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static List<SignalBindingInfo> CollectBindings(
|
||||
SourceProductionContext context,
|
||||
TypeGroup group,
|
||||
IReadOnlyDictionary<MethodCandidate, IReadOnlyList<AttributeData>> methodAttributes,
|
||||
INamedTypeSymbol godotNodeSymbol)
|
||||
{
|
||||
var bindings = new List<SignalBindingInfo>();
|
||||
|
||||
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<IMethodSymbol>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,11 +259,7 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
|
||||
{
|
||||
return typeSymbol.GetMembers()
|
||||
.OfType<IMethodSymbol>()
|
||||
.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<FieldCandidate> Fields { get; } = new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,7 +126,27 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
|
||||
|
||||
var explicitMappings = new Dictionary<string, List<INamedTypeSymbol>>(StringComparer.Ordinal);
|
||||
var implicitCandidates = new Dictionary<string, List<INamedTypeSymbol>>(StringComparer.Ordinal);
|
||||
CollectMappingCandidates(
|
||||
context,
|
||||
typeCandidates,
|
||||
autoLoadAttributeSymbol,
|
||||
godotNodeSymbol,
|
||||
projectAutoLoadNames,
|
||||
explicitMappings,
|
||||
implicitCandidates);
|
||||
|
||||
return ResolveTypedMappings(context, projectAutoLoadNames, explicitMappings, implicitCandidates);
|
||||
}
|
||||
|
||||
private static void CollectMappingCandidates(
|
||||
SourceProductionContext context,
|
||||
IReadOnlyList<GodotTypeCandidate> typeCandidates,
|
||||
INamedTypeSymbol? autoLoadAttributeSymbol,
|
||||
INamedTypeSymbol godotNodeSymbol,
|
||||
ISet<string> projectAutoLoadNames,
|
||||
IDictionary<string, List<INamedTypeSymbol>> explicitMappings,
|
||||
IDictionary<string, List<INamedTypeSymbol>> 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<string, INamedTypeSymbol> ResolveTypedMappings(
|
||||
SourceProductionContext context,
|
||||
IEnumerable<string> projectAutoLoadNames,
|
||||
IReadOnlyDictionary<string, List<INamedTypeSymbol>> explicitMappings,
|
||||
IReadOnlyDictionary<string, List<INamedTypeSymbol>> implicitCandidates)
|
||||
{
|
||||
var resolvedMappings = new Dictionary<string, INamedTypeSymbol>(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(" /// <summary>");
|
||||
builder.AppendLine($" /// 获取 AutoLoad <c>{member.AutoLoadName}</c>。");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
builder.AppendLine(
|
||||
$" public static {member.TypeName} {member.Identifier} => GetRequiredNode<{member.TypeName}>({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)});");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" /// <summary>");
|
||||
builder.AppendLine($" /// 尝试获取 AutoLoad <c>{member.AutoLoadName}</c>。");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
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(" /// <summary>");
|
||||
builder.AppendLine($" /// 获取 AutoLoad <c>{member.AutoLoadName}</c>。");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
builder.AppendLine(
|
||||
$" public static {member.TypeName} {member.Identifier} => GetRequiredNode<{member.TypeName}>({SymbolDisplay.FormatLiteral(member.AutoLoadName, true)});");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" /// <summary>");
|
||||
builder.AppendLine($" /// 尝试获取 AutoLoad <c>{member.AutoLoadName}</c>。");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
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(" /// <summary>");
|
||||
builder.AppendLine(" /// 获取一个必填的 AutoLoad 节点;缺失时抛出异常。");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
@ -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(" /// <summary>");
|
||||
builder.AppendLine(" /// 尝试从当前 SceneTree 根节点解析 AutoLoad。");
|
||||
builder.AppendLine(" /// </summary>");
|
||||
@ -470,9 +517,6 @@ public sealed class GodotProjectMetadataGenerator : IIncrementalGenerator
|
||||
builder.AppendLine(" value = root.GetNodeOrNull<TNode>($\"/root/{autoLoadName}\");");
|
||||
builder.AppendLine(" return value is not null;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine("}");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateInputActionsSource(IReadOnlyList<GeneratedInputActionMember> 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<string> seenAutoLoads,
|
||||
ICollection<ProjectAutoLoadEntry> autoLoads,
|
||||
ICollection<Diagnostic> 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<string> seenInputActions,
|
||||
ICollection<string> inputActions,
|
||||
ICollection<Diagnostic> 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();
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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、测试或单模块可局部验证的问题
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user