mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-23 19:24:29 +08:00
Compare commits
No commits in common. "main" and "v0.0.206" have entirely different histories.
@ -30,6 +30,12 @@ public sealed class WaitForAllCoroutines(
|
|||||||
/// 获取一个值,指示所有协程是否已完成执行
|
/// 获取一个值,指示所有协程是否已完成执行
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>当所有协程都已完成时返回true,否则返回false</returns>
|
/// <returns>当所有协程都已完成时返回true,否则返回false</returns>
|
||||||
// 检查所有协程句柄是否都不在调度器中存活
|
public bool IsDone
|
||||||
public bool IsDone => _handles.All(handle => !_scheduler.IsCoroutineAlive(handle));
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// 检查所有协程句柄是否都不在调度器中存活
|
||||||
|
return _handles.All(handle => !_scheduler.IsCoroutineAlive(handle));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,40 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 标记 Godot 节点字段,Source Generator 会为其生成节点获取逻辑。
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class GetNodeAttribute : Attribute
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 初始化 <see cref="GetNodeAttribute" /> 的新实例。
|
|
||||||
/// </summary>
|
|
||||||
public GetNodeAttribute()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 初始化 <see cref="GetNodeAttribute" /> 的新实例,并指定节点路径。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">节点路径。</param>
|
|
||||||
public GetNodeAttribute(string path)
|
|
||||||
{
|
|
||||||
Path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置节点路径。未设置时将根据字段名推导。
|
|
||||||
/// </summary>
|
|
||||||
public string? Path { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置节点是否必填。默认为 true。
|
|
||||||
/// </summary>
|
|
||||||
public bool Required { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取或设置节点查找模式。默认为 <see cref="NodeLookupMode.Auto" />。
|
|
||||||
/// </summary>
|
|
||||||
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 节点路径的查找模式。
|
|
||||||
/// </summary>
|
|
||||||
public enum NodeLookupMode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 自动推断。未显式设置路径时默认按唯一名查找。
|
|
||||||
/// </summary>
|
|
||||||
Auto = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 按唯一名查找,对应 Godot 的 %Name 语法。
|
|
||||||
/// </summary>
|
|
||||||
UniqueName = 1,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 按相对路径查找。
|
|
||||||
/// </summary>
|
|
||||||
RelativePath = 2,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 按绝对路径查找。
|
|
||||||
/// </summary>
|
|
||||||
AbsolutePath = 3
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
|
||||||
using Microsoft.CodeAnalysis.Testing;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Tests.Core;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 提供源代码生成器测试的通用功能。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TGenerator">要测试的源代码生成器类型,必须具有无参构造函数。</typeparam>
|
|
||||||
public static class GeneratorTest<TGenerator>
|
|
||||||
where TGenerator : new()
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 运行源代码生成器测试。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="source">输入源代码。</param>
|
|
||||||
/// <param name="generatedSources">期望生成的源文件集合。</param>
|
|
||||||
public static async Task RunAsync(
|
|
||||||
string source,
|
|
||||||
params (string filename, string content)[] generatedSources)
|
|
||||||
{
|
|
||||||
var test = new CSharpSourceGeneratorTest<TGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var (filename, content) in generatedSources)
|
|
||||||
test.TestState.GeneratedSources.Add(
|
|
||||||
(typeof(TGenerator), filename, content));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TestTargetFrameworks Condition="'$(TestTargetFrameworks)' == ''">net10.0</TestTargetFrameworks>
|
|
||||||
<TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks>
|
|
||||||
<ImplicitUsings>disable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<IsTestProject>true</IsTestProject>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.14.0"/>
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0"/>
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.14.0"/>
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0"/>
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.3"/>
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0"/>
|
|
||||||
<PackageReference Include="NUnit" Version="4.5.1"/>
|
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0"/>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\GFramework.Godot.SourceGenerators\GFramework.Godot.SourceGenerators.csproj"/>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@ -1,243 +0,0 @@
|
|||||||
using GFramework.Godot.SourceGenerators.Tests.Core;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
|
||||||
using Microsoft.CodeAnalysis.Testing;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Tests.GetNode;
|
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
public class GetNodeGeneratorTests
|
|
||||||
{
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_InferredUniqueNameBindings_And_ReadyHook_WhenReadyIsMissing()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
using System;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class GetNodeAttribute : Attribute
|
|
||||||
{
|
|
||||||
public GetNodeAttribute() {}
|
|
||||||
public GetNodeAttribute(string path) { Path = path; }
|
|
||||||
public string? Path { get; set; }
|
|
||||||
public bool Required { get; set; } = true;
|
|
||||||
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum NodeLookupMode
|
|
||||||
{
|
|
||||||
Auto = 0,
|
|
||||||
UniqueName = 1,
|
|
||||||
RelativePath = 2,
|
|
||||||
AbsolutePath = 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node
|
|
||||||
{
|
|
||||||
public virtual void _Ready() {}
|
|
||||||
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
|
||||||
public T? GetNodeOrNull<T>(string path) where T : Node => default;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HBoxContainer : Node
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public partial class TopBar : HBoxContainer
|
|
||||||
{
|
|
||||||
[GetNode]
|
|
||||||
private HBoxContainer _leftContainer = null!;
|
|
||||||
|
|
||||||
[GetNode]
|
|
||||||
private HBoxContainer m_rightContainer = null!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class TopBar
|
|
||||||
{
|
|
||||||
private void __InjectGetNodes_Generated()
|
|
||||||
{
|
|
||||||
_leftContainer = GetNode<global::Godot.HBoxContainer>("%LeftContainer");
|
|
||||||
m_rightContainer = GetNode<global::Godot.HBoxContainer>("%RightContainer");
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnGetNodeReadyGenerated();
|
|
||||||
|
|
||||||
public override void _Ready()
|
|
||||||
{
|
|
||||||
__InjectGetNodes_Generated();
|
|
||||||
OnGetNodeReadyGenerated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
await GeneratorTest<GetNodeGenerator>.RunAsync(
|
|
||||||
source,
|
|
||||||
("TestApp_TopBar.GetNode.g.cs", expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Generates_ManualInjectionOnly_WhenReadyAlreadyExists()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
using System;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class GetNodeAttribute : Attribute
|
|
||||||
{
|
|
||||||
public GetNodeAttribute() {}
|
|
||||||
public GetNodeAttribute(string path) { Path = path; }
|
|
||||||
public string? Path { get; set; }
|
|
||||||
public bool Required { get; set; } = true;
|
|
||||||
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum NodeLookupMode
|
|
||||||
{
|
|
||||||
Auto = 0,
|
|
||||||
UniqueName = 1,
|
|
||||||
RelativePath = 2,
|
|
||||||
AbsolutePath = 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node
|
|
||||||
{
|
|
||||||
public virtual void _Ready() {}
|
|
||||||
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
|
||||||
public T? GetNodeOrNull<T>(string path) where T : Node => default;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class HBoxContainer : Node
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public partial class TopBar : HBoxContainer
|
|
||||||
{
|
|
||||||
[GetNode("%LeftContainer")]
|
|
||||||
private HBoxContainer _leftContainer = null!;
|
|
||||||
|
|
||||||
[GetNode(Required = false, Lookup = NodeLookupMode.RelativePath)]
|
|
||||||
private HBoxContainer? _rightContainer;
|
|
||||||
|
|
||||||
public override void _Ready()
|
|
||||||
{
|
|
||||||
__InjectGetNodes_Generated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
const string expected = """
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace TestApp;
|
|
||||||
|
|
||||||
partial class TopBar
|
|
||||||
{
|
|
||||||
private void __InjectGetNodes_Generated()
|
|
||||||
{
|
|
||||||
_leftContainer = GetNode<global::Godot.HBoxContainer>("%LeftContainer");
|
|
||||||
_rightContainer = GetNodeOrNull<global::Godot.HBoxContainer>("RightContainer");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
""";
|
|
||||||
|
|
||||||
await GeneratorTest<GetNodeGenerator>.RunAsync(
|
|
||||||
source,
|
|
||||||
("TestApp_TopBar.GetNode.g.cs", expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task Reports_Diagnostic_When_FieldType_IsNotGodotNode()
|
|
||||||
{
|
|
||||||
const string source = """
|
|
||||||
using System;
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class GetNodeAttribute : Attribute
|
|
||||||
{
|
|
||||||
public string? Path { get; set; }
|
|
||||||
public bool Required { get; set; } = true;
|
|
||||||
public NodeLookupMode Lookup { get; set; } = NodeLookupMode.Auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum NodeLookupMode
|
|
||||||
{
|
|
||||||
Auto = 0,
|
|
||||||
UniqueName = 1,
|
|
||||||
RelativePath = 2,
|
|
||||||
AbsolutePath = 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace Godot
|
|
||||||
{
|
|
||||||
public class Node
|
|
||||||
{
|
|
||||||
public virtual void _Ready() {}
|
|
||||||
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
|
||||||
public T? GetNodeOrNull<T>(string path) where T : Node => default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace TestApp
|
|
||||||
{
|
|
||||||
public partial class TopBar : Node
|
|
||||||
{
|
|
||||||
[GetNode]
|
|
||||||
private string _leftContainer = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<GetNodeGenerator, DefaultVerifier>
|
|
||||||
{
|
|
||||||
TestState =
|
|
||||||
{
|
|
||||||
Sources = { source }
|
|
||||||
},
|
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
|
||||||
};
|
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_GetNode_004", DiagnosticSeverity.Error)
|
|
||||||
.WithSpan(39, 24, 39, 38)
|
|
||||||
.WithArguments("_leftContainer"));
|
|
||||||
|
|
||||||
await test.RunAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
// Copyright (c) 2025 GeWuYou
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
global using System;
|
|
||||||
global using System.Collections.Generic;
|
|
||||||
global using System.Linq;
|
|
||||||
global using System.Threading;
|
|
||||||
global using System.Threading.Tasks;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
; Shipped analyzer releases
|
|
||||||
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
|
|
||||||
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
; Unshipped analyzer release
|
|
||||||
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
|
|
||||||
|
|
||||||
### New Rules
|
|
||||||
|
|
||||||
Rule ID | Category | Severity | Notes
|
|
||||||
----------------------|------------------|----------|--------------------
|
|
||||||
GF_Godot_GetNode_001 | GFramework.Godot | Error | GetNodeDiagnostics
|
|
||||||
GF_Godot_GetNode_002 | GFramework.Godot | Error | GetNodeDiagnostics
|
|
||||||
GF_Godot_GetNode_003 | GFramework.Godot | Error | GetNodeDiagnostics
|
|
||||||
GF_Godot_GetNode_004 | GFramework.Godot | Error | GetNodeDiagnostics
|
|
||||||
GF_Godot_GetNode_005 | GFramework.Godot | Error | GetNodeDiagnostics
|
|
||||||
GF_Godot_GetNode_006 | GFramework.Godot | Warning | GetNodeDiagnostics
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
using GFramework.SourceGenerators.Common.Constants;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators.Diagnostics;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// GetNode 生成器相关诊断。
|
|
||||||
/// </summary>
|
|
||||||
public static class GetNodeDiagnostics
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 嵌套类型不受支持。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor NestedClassNotSupported =
|
|
||||||
new(
|
|
||||||
"GF_Godot_GetNode_001",
|
|
||||||
"Nested classes are not supported",
|
|
||||||
"Class '{0}' cannot use [GetNode] inside a nested type",
|
|
||||||
PathContests.GodotNamespace,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// static 字段不受支持。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor StaticFieldNotSupported =
|
|
||||||
new(
|
|
||||||
"GF_Godot_GetNode_002",
|
|
||||||
"Static fields are not supported",
|
|
||||||
"Field '{0}' cannot be static when using [GetNode]",
|
|
||||||
PathContests.GodotNamespace,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// readonly 字段不受支持。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor ReadOnlyFieldNotSupported =
|
|
||||||
new(
|
|
||||||
"GF_Godot_GetNode_003",
|
|
||||||
"Readonly fields are not supported",
|
|
||||||
"Field '{0}' cannot be readonly when using [GetNode]",
|
|
||||||
PathContests.GodotNamespace,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 字段类型必须继承自 Godot.Node。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor FieldTypeMustDeriveFromNode =
|
|
||||||
new(
|
|
||||||
"GF_Godot_GetNode_004",
|
|
||||||
"Field type must derive from Godot.Node",
|
|
||||||
"Field '{0}' must be a Godot.Node type to use [GetNode]",
|
|
||||||
PathContests.GodotNamespace,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 无法从字段名推导路径。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor CannotInferNodePath =
|
|
||||||
new(
|
|
||||||
"GF_Godot_GetNode_005",
|
|
||||||
"Cannot infer node path",
|
|
||||||
"Field '{0}' does not provide a path and its name cannot be converted to a node path",
|
|
||||||
PathContests.GodotNamespace,
|
|
||||||
DiagnosticSeverity.Error,
|
|
||||||
true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 现有 _Ready 中未调用生成注入逻辑。
|
|
||||||
/// </summary>
|
|
||||||
public static readonly DiagnosticDescriptor ManualReadyHookRequired =
|
|
||||||
new(
|
|
||||||
"GF_Godot_GetNode_006",
|
|
||||||
"Call generated injection from _Ready",
|
|
||||||
"Class '{0}' defines _Ready(); call __InjectGetNodes_Generated() there or remove _Ready() to use the generated hook",
|
|
||||||
PathContests.GodotNamespace,
|
|
||||||
DiagnosticSeverity.Warning,
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
@ -60,6 +60,11 @@
|
|||||||
<None Include="GeWuYou.$(AssemblyName).targets" Pack="true" PackagePath="build" Visible="false"/>
|
<None Include="GeWuYou.$(AssemblyName).targets" Pack="true" PackagePath="build" Visible="false"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Diagnostics\"/>
|
<Folder Include="diagnostics\"/>
|
||||||
|
<Folder Include="logging\"/>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md"/>
|
||||||
|
<AdditionalFiles Remove="AnalyzerReleases.Unshipped.md"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,568 +0,0 @@
|
|||||||
using System.Collections.Immutable;
|
|
||||||
using System.Text;
|
|
||||||
using GFramework.Godot.SourceGenerators.Diagnostics;
|
|
||||||
using GFramework.SourceGenerators.Common.Constants;
|
|
||||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
|
||||||
using GFramework.SourceGenerators.Common.Extensions;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.SourceGenerators;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 为带有 <c>[GetNode]</c> 的字段生成 Godot 节点获取逻辑。
|
|
||||||
/// </summary>
|
|
||||||
[Generator]
|
|
||||||
public sealed class GetNodeGenerator : IIncrementalGenerator
|
|
||||||
{
|
|
||||||
private const string GodotAbsolutePathPrefix = "/";
|
|
||||||
private const string GodotUniqueNamePrefix = "%";
|
|
||||||
|
|
||||||
private const string GetNodeAttributeMetadataName =
|
|
||||||
$"{PathContests.GodotSourceGeneratorsAbstractionsPath}.GetNodeAttribute";
|
|
||||||
|
|
||||||
private const string GetNodeLookupModeMetadataName =
|
|
||||||
$"{PathContests.GodotSourceGeneratorsAbstractionsPath}.NodeLookupMode";
|
|
||||||
|
|
||||||
private const string InjectionMethodName = "__InjectGetNodes_Generated";
|
|
||||||
private const string ReadyHookMethodName = "OnGetNodeReadyGenerated";
|
|
||||||
|
|
||||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
||||||
{
|
|
||||||
var candidates = context.SyntaxProvider.CreateSyntaxProvider(
|
|
||||||
static (node, _) => IsCandidate(node),
|
|
||||||
static (ctx, _) => Transform(ctx))
|
|
||||||
.Where(static candidate => candidate is not null);
|
|
||||||
|
|
||||||
var compilationAndCandidates = context.CompilationProvider.Combine(candidates.Collect());
|
|
||||||
|
|
||||||
context.RegisterSourceOutput(compilationAndCandidates,
|
|
||||||
static (spc, pair) => Execute(spc, pair.Left, pair.Right));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsCandidate(SyntaxNode node)
|
|
||||||
{
|
|
||||||
if (node is not VariableDeclaratorSyntax
|
|
||||||
{
|
|
||||||
Parent: VariableDeclarationSyntax
|
|
||||||
{
|
|
||||||
Parent: FieldDeclarationSyntax fieldDeclaration
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return fieldDeclaration.AttributeLists
|
|
||||||
.SelectMany(static list => list.Attributes)
|
|
||||||
.Any(static attribute => attribute.Name.ToString().Contains("GetNode", StringComparison.Ordinal));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static FieldCandidate? Transform(GeneratorSyntaxContext context)
|
|
||||||
{
|
|
||||||
if (context.Node is not VariableDeclaratorSyntax variable)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (ModelExtensions.GetDeclaredSymbol(context.SemanticModel, variable) is not IFieldSymbol fieldSymbol)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new FieldCandidate(variable, fieldSymbol);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Execute(
|
|
||||||
SourceProductionContext context,
|
|
||||||
Compilation compilation,
|
|
||||||
ImmutableArray<FieldCandidate?> candidates)
|
|
||||||
{
|
|
||||||
if (candidates.IsDefaultOrEmpty)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var getNodeAttribute = compilation.GetTypeByMetadataName(GetNodeAttributeMetadataName);
|
|
||||||
var godotNodeSymbol = compilation.GetTypeByMetadataName("Godot.Node");
|
|
||||||
|
|
||||||
if (getNodeAttribute is null || godotNodeSymbol is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var fieldCandidates = candidates
|
|
||||||
.Where(static candidate => candidate is not null)
|
|
||||||
.Select(static candidate => candidate!)
|
|
||||||
.Where(candidate => ResolveAttribute(candidate.FieldSymbol, getNodeAttribute) is not null)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var group in GroupByContainingType(fieldCandidates))
|
|
||||||
{
|
|
||||||
var typeSymbol = group.TypeSymbol;
|
|
||||||
|
|
||||||
if (!CanGenerateForType(context, group, typeSymbol))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var bindings = new List<NodeBindingInfo>();
|
|
||||||
|
|
||||||
foreach (var candidate in group.Fields)
|
|
||||||
{
|
|
||||||
var attribute = ResolveAttribute(candidate.FieldSymbol, getNodeAttribute);
|
|
||||||
if (attribute is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
bindings.Add(binding);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bindings.Count == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ReportMissingReadyHookCall(context, group, typeSymbol);
|
|
||||||
|
|
||||||
var source = GenerateSource(typeSymbol, bindings, FindReadyMethod(typeSymbol) is null);
|
|
||||||
context.AddSource(GetHintName(typeSymbol), source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CanGenerateForType(
|
|
||||||
SourceProductionContext context,
|
|
||||||
TypeGroup group,
|
|
||||||
INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
if (typeSymbol.ContainingType is not null)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
GetNodeDiagnostics.NestedClassNotSupported,
|
|
||||||
group.Fields[0].Variable.Identifier.GetLocation(),
|
|
||||||
typeSymbol.Name));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsPartial(typeSymbol))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
CommonDiagnostics.ClassMustBePartial,
|
|
||||||
group.Fields[0].Variable.Identifier.GetLocation(),
|
|
||||||
typeSymbol.Name));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryCreateBinding(
|
|
||||||
SourceProductionContext context,
|
|
||||||
FieldCandidate candidate,
|
|
||||||
AttributeData attribute,
|
|
||||||
INamedTypeSymbol godotNodeSymbol,
|
|
||||||
out NodeBindingInfo binding)
|
|
||||||
{
|
|
||||||
binding = default!;
|
|
||||||
|
|
||||||
if (candidate.FieldSymbol.IsStatic)
|
|
||||||
{
|
|
||||||
ReportFieldDiagnostic(context,
|
|
||||||
GetNodeDiagnostics.StaticFieldNotSupported,
|
|
||||||
candidate);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidate.FieldSymbol.IsReadOnly)
|
|
||||||
{
|
|
||||||
ReportFieldDiagnostic(context,
|
|
||||||
GetNodeDiagnostics.ReadOnlyFieldNotSupported,
|
|
||||||
candidate);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsGodotNodeType(candidate.FieldSymbol.Type, godotNodeSymbol))
|
|
||||||
{
|
|
||||||
ReportFieldDiagnostic(context,
|
|
||||||
GetNodeDiagnostics.FieldTypeMustDeriveFromNode,
|
|
||||||
candidate);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryResolvePath(candidate.FieldSymbol, attribute, out var path))
|
|
||||||
{
|
|
||||||
ReportFieldDiagnostic(context,
|
|
||||||
GetNodeDiagnostics.CannotInferNodePath,
|
|
||||||
candidate);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
binding = new NodeBindingInfo(
|
|
||||||
candidate.FieldSymbol,
|
|
||||||
path,
|
|
||||||
ResolveRequired(attribute));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReportFieldDiagnostic(
|
|
||||||
SourceProductionContext context,
|
|
||||||
DiagnosticDescriptor descriptor,
|
|
||||||
FieldCandidate candidate)
|
|
||||||
{
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
descriptor,
|
|
||||||
candidate.Variable.Identifier.GetLocation(),
|
|
||||||
candidate.FieldSymbol.Name));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ReportMissingReadyHookCall(
|
|
||||||
SourceProductionContext context,
|
|
||||||
TypeGroup group,
|
|
||||||
INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
var readyMethod = FindReadyMethod(typeSymbol);
|
|
||||||
if (readyMethod is null || CallsGeneratedInjection(readyMethod))
|
|
||||||
return;
|
|
||||||
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(
|
|
||||||
GetNodeDiagnostics.ManualReadyHookRequired,
|
|
||||||
readyMethod.Locations.FirstOrDefault() ?? group.Fields[0].Variable.Identifier.GetLocation(),
|
|
||||||
typeSymbol.Name));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AttributeData? ResolveAttribute(
|
|
||||||
IFieldSymbol fieldSymbol,
|
|
||||||
INamedTypeSymbol getNodeAttribute)
|
|
||||||
{
|
|
||||||
return fieldSymbol.GetAttributes()
|
|
||||||
.FirstOrDefault(attribute =>
|
|
||||||
SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, getNodeAttribute));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsPartial(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
return typeSymbol.DeclaringSyntaxReferences
|
|
||||||
.Select(static reference => reference.GetSyntax())
|
|
||||||
.OfType<ClassDeclarationSyntax>()
|
|
||||||
.All(static declaration =>
|
|
||||||
declaration.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.PartialKeyword)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsGodotNodeType(ITypeSymbol typeSymbol, INamedTypeSymbol godotNodeSymbol)
|
|
||||||
{
|
|
||||||
var current = typeSymbol as INamedTypeSymbol;
|
|
||||||
while (current is not null)
|
|
||||||
{
|
|
||||||
if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, godotNodeSymbol) ||
|
|
||||||
SymbolEqualityComparer.Default.Equals(current, godotNodeSymbol))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
current = current.BaseType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IMethodSymbol? FindReadyMethod(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
return typeSymbol.GetMembers()
|
|
||||||
.OfType<IMethodSymbol>()
|
|
||||||
.FirstOrDefault(static method =>
|
|
||||||
method.Name == "_Ready" &&
|
|
||||||
!method.IsStatic &&
|
|
||||||
method.Parameters.Length == 0 &&
|
|
||||||
method.MethodKind == MethodKind.Ordinary);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CallsGeneratedInjection(IMethodSymbol readyMethod)
|
|
||||||
{
|
|
||||||
foreach (var syntaxReference in readyMethod.DeclaringSyntaxReferences)
|
|
||||||
{
|
|
||||||
if (syntaxReference.GetSyntax() is not MethodDeclarationSyntax methodSyntax)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (methodSyntax.DescendantNodes()
|
|
||||||
.OfType<InvocationExpressionSyntax>()
|
|
||||||
.Any(IsGeneratedInjectionInvocation))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsGeneratedInjectionInvocation(InvocationExpressionSyntax invocation)
|
|
||||||
{
|
|
||||||
switch (invocation.Expression)
|
|
||||||
{
|
|
||||||
case IdentifierNameSyntax identifierName:
|
|
||||||
return string.Equals(
|
|
||||||
identifierName.Identifier.ValueText,
|
|
||||||
InjectionMethodName,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
case MemberAccessExpressionSyntax memberAccess:
|
|
||||||
return string.Equals(
|
|
||||||
memberAccess.Name.Identifier.ValueText,
|
|
||||||
InjectionMethodName,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ResolveRequired(AttributeData attribute)
|
|
||||||
{
|
|
||||||
return attribute.GetNamedArgument("Required", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryResolvePath(
|
|
||||||
IFieldSymbol fieldSymbol,
|
|
||||||
AttributeData attribute,
|
|
||||||
out string path)
|
|
||||||
{
|
|
||||||
var explicitPath = ResolveExplicitPath(attribute);
|
|
||||||
if (!string.IsNullOrWhiteSpace(explicitPath))
|
|
||||||
return ReturnResolvedPath(explicitPath!, out path);
|
|
||||||
|
|
||||||
var inferredName = InferNodeName(fieldSymbol.Name);
|
|
||||||
if (string.IsNullOrWhiteSpace(inferredName))
|
|
||||||
{
|
|
||||||
path = string.Empty;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var resolvedName = inferredName!;
|
|
||||||
return TryResolveInferredPath(attribute, resolvedName, out path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ReturnResolvedPath(string resolvedPath, out string path)
|
|
||||||
{
|
|
||||||
path = resolvedPath;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryResolveInferredPath(
|
|
||||||
AttributeData attribute,
|
|
||||||
string inferredName,
|
|
||||||
out string path)
|
|
||||||
{
|
|
||||||
path = BuildPathPrefix(ResolveLookup(attribute)) + inferredName;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildPathPrefix(NodeLookupModeValue lookupMode)
|
|
||||||
{
|
|
||||||
switch (lookupMode)
|
|
||||||
{
|
|
||||||
case NodeLookupModeValue.RelativePath:
|
|
||||||
return string.Empty;
|
|
||||||
case NodeLookupModeValue.AbsolutePath:
|
|
||||||
return GodotAbsolutePathPrefix;
|
|
||||||
default:
|
|
||||||
return GodotUniqueNamePrefix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? ResolveExplicitPath(AttributeData attribute)
|
|
||||||
{
|
|
||||||
var namedPath = attribute.GetNamedArgument<string>("Path");
|
|
||||||
if (!string.IsNullOrWhiteSpace(namedPath))
|
|
||||||
return namedPath;
|
|
||||||
|
|
||||||
if (attribute.ConstructorArguments.Length == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return attribute.ConstructorArguments[0].Value as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static NodeLookupModeValue ResolveLookup(AttributeData attribute)
|
|
||||||
{
|
|
||||||
foreach (var namedArgument in attribute.NamedArguments)
|
|
||||||
{
|
|
||||||
if (!string.Equals(namedArgument.Key, "Lookup", StringComparison.Ordinal))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (namedArgument.Value.Type?.ToDisplayString() != GetNodeLookupModeMetadataName)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (namedArgument.Value.Value is int value)
|
|
||||||
return (NodeLookupModeValue)value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeLookupModeValue.Auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? InferNodeName(string fieldName)
|
|
||||||
{
|
|
||||||
var workingName = fieldName.TrimStart('_');
|
|
||||||
if (workingName.StartsWith("m_", StringComparison.OrdinalIgnoreCase))
|
|
||||||
workingName = workingName.Substring(2);
|
|
||||||
|
|
||||||
workingName = workingName.TrimStart('_');
|
|
||||||
if (string.IsNullOrWhiteSpace(workingName))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (workingName.IndexOfAny(['_', '-', ' ']) >= 0)
|
|
||||||
{
|
|
||||||
var parts = workingName
|
|
||||||
.Split(['_', '-', ' '], StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
return parts.Length == 0
|
|
||||||
? null
|
|
||||||
: string.Concat(parts.Select(ToPascalToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ToPascalToken(workingName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ToPascalToken(string token)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(token))
|
|
||||||
return token;
|
|
||||||
|
|
||||||
if (token.Length == 1)
|
|
||||||
return token.ToUpperInvariant();
|
|
||||||
|
|
||||||
return char.ToUpperInvariant(token[0]) + token.Substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GenerateSource(
|
|
||||||
INamedTypeSymbol typeSymbol,
|
|
||||||
IReadOnlyList<NodeBindingInfo> bindings,
|
|
||||||
bool generateReadyOverride)
|
|
||||||
{
|
|
||||||
var namespaceName = typeSymbol.GetNamespace();
|
|
||||||
var generics = typeSymbol.ResolveGenerics();
|
|
||||||
|
|
||||||
var sb = new StringBuilder()
|
|
||||||
.AppendLine("// <auto-generated />")
|
|
||||||
.AppendLine("#nullable enable");
|
|
||||||
|
|
||||||
if (namespaceName is not null)
|
|
||||||
{
|
|
||||||
sb.AppendLine()
|
|
||||||
.AppendLine($"namespace {namespaceName};");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine()
|
|
||||||
.AppendLine($"partial class {typeSymbol.Name}{generics.Parameters}");
|
|
||||||
|
|
||||||
foreach (var constraint in generics.Constraints)
|
|
||||||
sb.AppendLine($" {constraint}");
|
|
||||||
|
|
||||||
sb.AppendLine("{")
|
|
||||||
.AppendLine($" private void {InjectionMethodName}()")
|
|
||||||
.AppendLine(" {");
|
|
||||||
|
|
||||||
foreach (var binding in bindings)
|
|
||||||
{
|
|
||||||
var typeName = binding.FieldSymbol.Type
|
|
||||||
.WithNullableAnnotation(NullableAnnotation.None)
|
|
||||||
.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
|
||||||
|
|
||||||
var accessor = binding.Required ? "GetNode" : "GetNodeOrNull";
|
|
||||||
var pathLiteral = EscapeStringLiteral(binding.Path);
|
|
||||||
sb.AppendLine(
|
|
||||||
$" {binding.FieldSymbol.Name} = {accessor}<{typeName}>(\"{pathLiteral}\");");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine(" }");
|
|
||||||
|
|
||||||
if (generateReadyOverride)
|
|
||||||
{
|
|
||||||
sb.AppendLine()
|
|
||||||
.AppendLine($" partial void {ReadyHookMethodName}();")
|
|
||||||
.AppendLine()
|
|
||||||
.AppendLine(" public override void _Ready()")
|
|
||||||
.AppendLine(" {")
|
|
||||||
.AppendLine($" {InjectionMethodName}();")
|
|
||||||
.AppendLine($" {ReadyHookMethodName}();")
|
|
||||||
.AppendLine(" }");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine("}");
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetHintName(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
|
|
||||||
.Replace("global::", string.Empty)
|
|
||||||
.Replace("<", "_")
|
|
||||||
.Replace(">", "_")
|
|
||||||
.Replace(",", "_")
|
|
||||||
.Replace(" ", string.Empty)
|
|
||||||
.Replace(".", "_") + ".GetNode.g.cs";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string EscapeStringLiteral(string value)
|
|
||||||
{
|
|
||||||
return value
|
|
||||||
.Replace("\\", "\\\\")
|
|
||||||
.Replace("\"", "\\\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IReadOnlyList<TypeGroup> GroupByContainingType(IEnumerable<FieldCandidate> candidates)
|
|
||||||
{
|
|
||||||
var groupMap = new Dictionary<INamedTypeSymbol, TypeGroup>(SymbolEqualityComparer.Default);
|
|
||||||
var orderedGroups = new List<TypeGroup>();
|
|
||||||
|
|
||||||
foreach (var candidate in candidates)
|
|
||||||
{
|
|
||||||
var typeSymbol = candidate.FieldSymbol.ContainingType;
|
|
||||||
if (!groupMap.TryGetValue(typeSymbol, out var group))
|
|
||||||
{
|
|
||||||
group = new TypeGroup(typeSymbol);
|
|
||||||
groupMap.Add(typeSymbol, group);
|
|
||||||
orderedGroups.Add(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
group.Fields.Add(candidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return orderedGroups;
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FieldCandidate
|
|
||||||
{
|
|
||||||
public FieldCandidate(
|
|
||||||
VariableDeclaratorSyntax variable,
|
|
||||||
IFieldSymbol fieldSymbol)
|
|
||||||
{
|
|
||||||
Variable = variable;
|
|
||||||
FieldSymbol = fieldSymbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
public VariableDeclaratorSyntax Variable { get; }
|
|
||||||
|
|
||||||
public IFieldSymbol FieldSymbol { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class NodeBindingInfo
|
|
||||||
{
|
|
||||||
public NodeBindingInfo(
|
|
||||||
IFieldSymbol fieldSymbol,
|
|
||||||
string path,
|
|
||||||
bool required)
|
|
||||||
{
|
|
||||||
FieldSymbol = fieldSymbol;
|
|
||||||
Path = path;
|
|
||||||
Required = required;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IFieldSymbol FieldSymbol { get; }
|
|
||||||
|
|
||||||
public string Path { get; }
|
|
||||||
|
|
||||||
public bool Required { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum NodeLookupModeValue
|
|
||||||
{
|
|
||||||
Auto = 0,
|
|
||||||
UniqueName = 1,
|
|
||||||
RelativePath = 2,
|
|
||||||
AbsolutePath = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TypeGroup
|
|
||||||
{
|
|
||||||
public TypeGroup(INamedTypeSymbol typeSymbol)
|
|
||||||
{
|
|
||||||
TypeSymbol = typeSymbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
public INamedTypeSymbol TypeSymbol { get; }
|
|
||||||
|
|
||||||
public List<FieldCandidate> Fields { get; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,40 +6,8 @@
|
|||||||
|
|
||||||
- 与 Godot 场景相关的编译期生成能力
|
- 与 Godot 场景相关的编译期生成能力
|
||||||
- 基于 Roslyn 的增量生成器实现
|
- 基于 Roslyn 的增量生成器实现
|
||||||
- `[GetNode]` 字段注入,减少 `_Ready()` 里的 `GetNode<T>()` 样板代码
|
|
||||||
|
|
||||||
## 使用建议
|
## 使用建议
|
||||||
|
|
||||||
- 仅在 Godot + C# 项目中启用
|
- 仅在 Godot + C# 项目中启用
|
||||||
- 非 Godot 项目可只使用 GFramework.SourceGenerators
|
- 非 Godot 项目可只使用 GFramework.SourceGenerators
|
||||||
|
|
||||||
## GetNode 用法
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
public partial class TopBar : HBoxContainer
|
|
||||||
{
|
|
||||||
[GetNode]
|
|
||||||
private HBoxContainer _leftContainer = null!;
|
|
||||||
|
|
||||||
[GetNode]
|
|
||||||
private HBoxContainer _rightContainer = null!;
|
|
||||||
|
|
||||||
public override void _Ready()
|
|
||||||
{
|
|
||||||
__InjectGetNodes_Generated();
|
|
||||||
OnReadyAfterGetNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnReadyAfterGetNode()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
当未显式填写路径时,生成器会默认将字段名推导为唯一名路径:
|
|
||||||
|
|
||||||
- `_leftContainer` -> `%LeftContainer`
|
|
||||||
- `m_rightContainer` -> `%RightContainer`
|
|
||||||
|
|||||||
@ -46,7 +46,6 @@
|
|||||||
<None Remove="GFramework.SourceGenerators\**"/>
|
<None Remove="GFramework.SourceGenerators\**"/>
|
||||||
<None Remove="GFramework.SourceGenerators.Common\**"/>
|
<None Remove="GFramework.SourceGenerators.Common\**"/>
|
||||||
<None Remove="GFramework.SourceGenerators.Tests\**"/>
|
<None Remove="GFramework.SourceGenerators.Tests\**"/>
|
||||||
<None Remove="GFramework.Godot.SourceGenerators.Tests\**"/>
|
|
||||||
<None Remove="GFramework.Godot.SourceGenerators.Abstractions\**"/>
|
<None Remove="GFramework.Godot.SourceGenerators.Abstractions\**"/>
|
||||||
<None Remove="GFramework.SourceGenerators.Abstractions\**"/>
|
<None Remove="GFramework.SourceGenerators.Abstractions\**"/>
|
||||||
<None Remove="GFramework.Core.Abstractions\**"/>
|
<None Remove="GFramework.Core.Abstractions\**"/>
|
||||||
@ -86,7 +85,6 @@
|
|||||||
<Compile Remove="GFramework.SourceGenerators\**"/>
|
<Compile Remove="GFramework.SourceGenerators\**"/>
|
||||||
<Compile Remove="GFramework.SourceGenerators.Common\**"/>
|
<Compile Remove="GFramework.SourceGenerators.Common\**"/>
|
||||||
<Compile Remove="GFramework.SourceGenerators.Tests\**"/>
|
<Compile Remove="GFramework.SourceGenerators.Tests\**"/>
|
||||||
<Compile Remove="GFramework.Godot.SourceGenerators.Tests\**"/>
|
|
||||||
<Compile Remove="GFramework.Godot.SourceGenerators.Abstractions\**"/>
|
<Compile Remove="GFramework.Godot.SourceGenerators.Abstractions\**"/>
|
||||||
<Compile Remove="GFramework.SourceGenerators.Abstractions\**"/>
|
<Compile Remove="GFramework.SourceGenerators.Abstractions\**"/>
|
||||||
<Compile Remove="GFramework.Core.Abstractions\**"/>
|
<Compile Remove="GFramework.Core.Abstractions\**"/>
|
||||||
@ -112,7 +110,6 @@
|
|||||||
<EmbeddedResource Remove="GFramework.SourceGenerators\**"/>
|
<EmbeddedResource Remove="GFramework.SourceGenerators\**"/>
|
||||||
<EmbeddedResource Remove="GFramework.SourceGenerators.Common\**"/>
|
<EmbeddedResource Remove="GFramework.SourceGenerators.Common\**"/>
|
||||||
<EmbeddedResource Remove="GFramework.SourceGenerators.Tests\**"/>
|
<EmbeddedResource Remove="GFramework.SourceGenerators.Tests\**"/>
|
||||||
<EmbeddedResource Remove="GFramework.Godot.SourceGenerators.Tests\**"/>
|
|
||||||
<EmbeddedResource Remove="GFramework.Godot.SourceGenerators.Abstractions\**"/>
|
<EmbeddedResource Remove="GFramework.Godot.SourceGenerators.Abstractions\**"/>
|
||||||
<EmbeddedResource Remove="GFramework.SourceGenerators.Abstractions\**"/>
|
<EmbeddedResource Remove="GFramework.SourceGenerators.Abstractions\**"/>
|
||||||
<EmbeddedResource Remove="GFramework.Core.Abstractions\**"/>
|
<EmbeddedResource Remove="GFramework.Core.Abstractions\**"/>
|
||||||
|
|||||||
@ -34,8 +34,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Ecs.Arch.Tests",
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game.Tests", "GFramework.Game.Tests\GFramework.Game.Tests.csproj", "{738DC58A-0387-4D75-AA96-1C1D8C29D350}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game.Tests", "GFramework.Game.Tests\GFramework.Game.Tests.csproj", "{738DC58A-0387-4D75-AA96-1C1D8C29D350}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Godot.SourceGenerators.Tests", "GFramework.Godot.SourceGenerators.Tests\GFramework.Godot.SourceGenerators.Tests.csproj", "{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -250,18 +248,6 @@ Global
|
|||||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x64.Build.0 = Release|Any CPU
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x86.ActiveCfg = Release|Any CPU
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x86.Build.0 = Release|Any CPU
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{E315489C-248A-4ABB-BD92-96F9F3AFE2C1}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
// meta-description: 负责管理场景的生命周期和架构关联
|
// meta-description: 负责管理场景的生命周期和架构关联
|
||||||
using Godot;
|
using Godot;
|
||||||
using GFramework.Core.Abstractions.Controller;
|
using GFramework.Core.Abstractions.Controller;
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
using GFramework.SourceGenerators.Abstractions.Logging;
|
using GFramework.SourceGenerators.Abstractions.Logging;
|
||||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
@ -17,15 +16,7 @@ public partial class _CLASS_ :_BASE_,IController
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
__InjectGetNodes_Generated();
|
|
||||||
OnReadyAfterGetNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 节点注入完成后的初始化钩子。
|
|
||||||
/// </summary>
|
|
||||||
private void OnReadyAfterGetNode()
|
|
||||||
{
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,6 @@ using GFramework.Core.Abstractions.Controller;
|
|||||||
using GFramework.Core.Extensions;
|
using GFramework.Core.Extensions;
|
||||||
using GFramework.Game.Abstractions.UI;
|
using GFramework.Game.Abstractions.UI;
|
||||||
using GFramework.Godot.UI;
|
using GFramework.Godot.UI;
|
||||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
|
||||||
using GFramework.SourceGenerators.Abstractions.Logging;
|
using GFramework.SourceGenerators.Abstractions.Logging;
|
||||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
@ -20,15 +19,7 @@ public partial class _CLASS_ :_BASE_,IController,IUiPageBehaviorProvider,IUiPage
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
__InjectGetNodes_Generated();
|
|
||||||
OnReadyAfterGetNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 节点注入完成后的初始化钩子。
|
|
||||||
/// </summary>
|
|
||||||
private void OnReadyAfterGetNode()
|
|
||||||
{
|
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 页面行为实例的私有字段
|
/// 页面行为实例的私有字段
|
||||||
@ -93,4 +84,4 @@ public partial class _CLASS_ :_BASE_,IController,IUiPageBehaviorProvider,IUiPage
|
|||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user