mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-31 18:39:00 +08:00
Merge pull request #151 from GeWuYou/feat/generator-skip-static-readonly-fields
feat(generator): 添加对 [GetAll] 特性的静态和只读字段跳过支持
This commit is contained in:
commit
a628ade28e
@ -25,6 +25,12 @@ public static class PathContests
|
||||
/// </summary>
|
||||
public const string GameNamespace = $"{BaseNamespace}.Game";
|
||||
|
||||
/// <summary>
|
||||
/// GFramework源代码生成器根命名空间
|
||||
/// </summary>
|
||||
public const string SourceGeneratorsPath = $"{BaseNamespace}.SourceGenerators";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// GFramework源代码生成器抽象层命名空间
|
||||
/// </summary>
|
||||
|
||||
120
GFramework.SourceGenerators.Tests/Core/MarkupTestSource.cs
Normal file
120
GFramework.SourceGenerators.Tests/Core/MarkupTestSource.cs
Normal file
@ -0,0 +1,120 @@
|
||||
using System.Text;
|
||||
|
||||
namespace GFramework.SourceGenerators.Tests.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 为源生成器测试提供轻量的源码标记解析能力。
|
||||
/// </summary>
|
||||
public sealed class MarkupTestSource
|
||||
{
|
||||
private readonly SourceText _sourceText;
|
||||
private readonly IReadOnlyDictionary<string, TextSpan> _spans;
|
||||
|
||||
private MarkupTestSource(
|
||||
string source,
|
||||
SourceText sourceText,
|
||||
IReadOnlyDictionary<string, TextSpan> spans)
|
||||
{
|
||||
Source = source;
|
||||
_sourceText = sourceText;
|
||||
_spans = spans;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取移除标记后的源码文本。
|
||||
/// </summary>
|
||||
public string Source { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 解析形如 <c>{|#0:identifier|}</c> 的单层标记,并保留去标记后的源码。
|
||||
/// </summary>
|
||||
/// <param name="markupSource">包含测试标记的源码。</param>
|
||||
/// <returns>可用于测试输入和诊断定位的解析结果。</returns>
|
||||
/// <exception cref="InvalidOperationException">标记格式不合法,或存在重复标记编号时抛出。</exception>
|
||||
public static MarkupTestSource Parse(string markupSource)
|
||||
{
|
||||
var builder = new StringBuilder(markupSource.Length);
|
||||
var spans = new Dictionary<string, TextSpan>(StringComparer.Ordinal);
|
||||
|
||||
for (var index = 0; index < markupSource.Length; index++)
|
||||
{
|
||||
if (!StartsWithMarker(markupSource, index))
|
||||
{
|
||||
builder.Append(markupSource[index]);
|
||||
continue;
|
||||
}
|
||||
|
||||
index += 3;
|
||||
var markerIdStart = index;
|
||||
while (index < markupSource.Length && markupSource[index] != ':')
|
||||
index++;
|
||||
|
||||
if (index >= markupSource.Length)
|
||||
throw new InvalidOperationException("Unterminated markup marker identifier.");
|
||||
|
||||
var markerId = markupSource.Substring(markerIdStart, index - markerIdStart);
|
||||
if (markerId.Length == 0)
|
||||
throw new InvalidOperationException("Markup marker identifier cannot be empty.");
|
||||
|
||||
var spanStart = builder.Length;
|
||||
index++;
|
||||
|
||||
while (index < markupSource.Length && !EndsWithMarker(markupSource, index))
|
||||
{
|
||||
builder.Append(markupSource[index]);
|
||||
index++;
|
||||
}
|
||||
|
||||
if (index >= markupSource.Length)
|
||||
throw new InvalidOperationException($"Unterminated markup marker '{markerId}'.");
|
||||
|
||||
if (!spans.TryAdd(markerId, TextSpan.FromBounds(spanStart, builder.Length)))
|
||||
throw new InvalidOperationException($"Duplicate markup marker '{markerId}'.");
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
var source = builder.ToString();
|
||||
return new MarkupTestSource(source, SourceText.From(source), spans);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将标记位置应用到诊断断言,避免测试依赖硬编码行列号。
|
||||
/// </summary>
|
||||
/// <param name="diagnosticResult">要补全定位信息的诊断断言。</param>
|
||||
/// <param name="markerId">标记编号。</param>
|
||||
/// <returns>包含定位信息的诊断断言。</returns>
|
||||
/// <exception cref="KeyNotFoundException">指定标记不存在时抛出。</exception>
|
||||
public DiagnosticResult WithSpan(
|
||||
DiagnosticResult diagnosticResult,
|
||||
string markerId)
|
||||
{
|
||||
var span = _spans[markerId];
|
||||
var lineSpan = _sourceText.Lines.GetLinePositionSpan(span);
|
||||
|
||||
return diagnosticResult.WithSpan(
|
||||
lineSpan.Start.Line + 1,
|
||||
lineSpan.Start.Character + 1,
|
||||
lineSpan.End.Line + 1,
|
||||
lineSpan.End.Character + 1);
|
||||
}
|
||||
|
||||
private static bool StartsWithMarker(
|
||||
string text,
|
||||
int index)
|
||||
{
|
||||
return index + 3 < text.Length &&
|
||||
text[index] == '{' &&
|
||||
text[index + 1] == '|' &&
|
||||
text[index + 2] == '#';
|
||||
}
|
||||
|
||||
private static bool EndsWithMarker(
|
||||
string text,
|
||||
int index)
|
||||
{
|
||||
return index + 1 < text.Length &&
|
||||
text[index] == '|' &&
|
||||
text[index + 1] == '}';
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ global using System.Linq;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
global using Microsoft.CodeAnalysis;
|
||||
global using Microsoft.CodeAnalysis.Text;
|
||||
global using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
global using Microsoft.CodeAnalysis.Testing;
|
||||
global using NUnit.Framework;
|
||||
@ -377,6 +377,291 @@ public class ContextGetGeneratorTests
|
||||
Assert.Pass();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Ignores_NonInferable_Const_Field_For_GetAll_Class_Without_Diagnostic()
|
||||
{
|
||||
var source = """
|
||||
using System;
|
||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class GetAllAttribute : Attribute { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Rule
|
||||
{
|
||||
public interface IContextAware { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Rule
|
||||
{
|
||||
public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Extensions
|
||||
{
|
||||
public static class ContextAwareServiceExtensions
|
||||
{
|
||||
public static T GetModel<T>(this object contextAware) => default!;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||
|
||||
[GetAll]
|
||||
public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
|
||||
{
|
||||
private const double LogicStep = 0.2;
|
||||
private IInventoryModel _model = null!;
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
using GFramework.Core.Extensions;
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
partial class BattlePanel
|
||||
{
|
||||
private void __InjectContextBindings_Generated()
|
||||
{
|
||||
_model = this.GetModel<global::TestApp.IInventoryModel>();
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
await GeneratorTest<ContextGetGenerator>.RunAsync(
|
||||
source,
|
||||
("TestApp_BattlePanel.ContextGet.g.cs", expected));
|
||||
Assert.Pass();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Warns_And_Skips_Readonly_Inferred_Field_For_GetAll_Class()
|
||||
{
|
||||
var source = MarkupTestSource.Parse("""
|
||||
using System;
|
||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class GetAllAttribute : Attribute { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Rule
|
||||
{
|
||||
public interface IContextAware { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Rule
|
||||
{
|
||||
public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Extensions
|
||||
{
|
||||
public static class ContextAwareServiceExtensions
|
||||
{
|
||||
public static T GetModel<T>(this object contextAware) => default!;
|
||||
public static T GetSystem<T>(this object contextAware) => default!;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||
public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { }
|
||||
|
||||
[GetAll]
|
||||
public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
|
||||
{
|
||||
private readonly IInventoryModel {|#0:_model|} = null!;
|
||||
private ICombatSystem _system = null!;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
using GFramework.Core.Extensions;
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
partial class BattlePanel
|
||||
{
|
||||
private void __InjectContextBindings_Generated()
|
||||
{
|
||||
_system = this.GetSystem<global::TestApp.ICombatSystem>();
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source.Source },
|
||||
GeneratedSources =
|
||||
{
|
||||
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
|
||||
}
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(source.WithSpan(
|
||||
new DiagnosticResult("GF_ContextGet_008", DiagnosticSeverity.Warning),
|
||||
"0")
|
||||
.WithArguments("_model"));
|
||||
|
||||
await test.RunAsync();
|
||||
Assert.Pass();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Warns_And_Skips_Static_Inferred_Field_For_GetAll_Class()
|
||||
{
|
||||
var source = MarkupTestSource.Parse("""
|
||||
using System;
|
||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public sealed class GetAllAttribute : Attribute { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Rule
|
||||
{
|
||||
public interface IContextAware { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Rule
|
||||
{
|
||||
public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.IContextAware { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Extensions
|
||||
{
|
||||
public static class ContextAwareServiceExtensions
|
||||
{
|
||||
public static T GetModel<T>(this object contextAware) => default!;
|
||||
public static T GetSystem<T>(this object contextAware) => default!;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||
public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { }
|
||||
|
||||
[GetAll]
|
||||
public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
|
||||
{
|
||||
private static IInventoryModel {|#0:_model|} = null!;
|
||||
private ICombatSystem _system = null!;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
using GFramework.Core.Extensions;
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
partial class BattlePanel
|
||||
{
|
||||
private void __InjectContextBindings_Generated()
|
||||
{
|
||||
_system = this.GetSystem<global::TestApp.ICombatSystem>();
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source.Source },
|
||||
GeneratedSources =
|
||||
{
|
||||
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
|
||||
}
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(source.WithSpan(
|
||||
new DiagnosticResult("GF_ContextGet_007", DiagnosticSeverity.Warning),
|
||||
"0")
|
||||
.WithArguments("_model"));
|
||||
|
||||
await test.RunAsync();
|
||||
Assert.Pass();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Skips_Nullable_Service_Like_Field_For_ContextAware_GetAll_Class()
|
||||
{
|
||||
@ -549,62 +834,63 @@ public class ContextGetGeneratorTests
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_When_Class_Is_Not_ContextAware()
|
||||
{
|
||||
var source = """
|
||||
using System;
|
||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||
var source = MarkupTestSource.Parse("""
|
||||
using System;
|
||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
|
||||
public sealed class GetModelAttribute : Attribute { }
|
||||
}
|
||||
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
|
||||
public sealed class GetModelAttribute : Attribute { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Extensions
|
||||
{
|
||||
public static class ContextAwareServiceExtensions
|
||||
{
|
||||
public static T GetModel<T>(this object contextAware) => default!;
|
||||
}
|
||||
}
|
||||
namespace GFramework.Core.Extensions
|
||||
{
|
||||
public static class ContextAwareServiceExtensions
|
||||
{
|
||||
public static T GetModel<T>(this object contextAware) => default!;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||
namespace TestApp
|
||||
{
|
||||
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||
|
||||
public partial class InventoryPanel
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel _model = null!;
|
||||
}
|
||||
}
|
||||
""";
|
||||
public partial class InventoryPanel
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel {|#0:_model|} = null!;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source }
|
||||
Sources = { source.Source }
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_005", DiagnosticSeverity.Error)
|
||||
.WithSpan(40, 33, 40, 39)
|
||||
test.ExpectedDiagnostics.Add(source.WithSpan(
|
||||
new DiagnosticResult("GF_ContextGet_005", DiagnosticSeverity.Error),
|
||||
"0")
|
||||
.WithArguments("InventoryPanel"));
|
||||
|
||||
await test.RunAsync();
|
||||
@ -614,74 +900,217 @@ public class ContextGetGeneratorTests
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_When_GetModels_Field_Is_Not_IReadOnlyList()
|
||||
{
|
||||
var source = """
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||
var source = MarkupTestSource.Parse("""
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
|
||||
public sealed class GetModelsAttribute : Attribute { }
|
||||
}
|
||||
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
|
||||
public sealed class GetModelsAttribute : Attribute { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Rule
|
||||
{
|
||||
public interface IContextAware { }
|
||||
}
|
||||
namespace GFramework.Core.Abstractions.Rule
|
||||
{
|
||||
public interface IContextAware { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Extensions
|
||||
{
|
||||
public static class ContextAwareServiceExtensions
|
||||
{
|
||||
public static IReadOnlyList<T> GetModels<T>(this object contextAware) => default!;
|
||||
}
|
||||
}
|
||||
namespace GFramework.Core.Extensions
|
||||
{
|
||||
public static class ContextAwareServiceExtensions
|
||||
{
|
||||
public static IReadOnlyList<T> GetModels<T>(this object contextAware) => default!;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||
namespace TestApp
|
||||
{
|
||||
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||
|
||||
public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
|
||||
{
|
||||
[GetModels]
|
||||
private List<IInventoryModel> _models = new();
|
||||
}
|
||||
}
|
||||
""";
|
||||
public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
|
||||
{
|
||||
[GetModels]
|
||||
private List<IInventoryModel> {|#0:_models|} = new();
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source }
|
||||
Sources = { source.Source }
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_004", DiagnosticSeverity.Error)
|
||||
.WithSpan(46, 39, 46, 46)
|
||||
test.ExpectedDiagnostics.Add(source.WithSpan(
|
||||
new DiagnosticResult("GF_ContextGet_004", DiagnosticSeverity.Error),
|
||||
"0")
|
||||
.WithArguments("_models", "System.Collections.Generic.List<TestApp.IInventoryModel>", "GetModels"));
|
||||
|
||||
await test.RunAsync();
|
||||
Assert.Pass();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_For_Readonly_Explicit_GetModel_Field()
|
||||
{
|
||||
var source = MarkupTestSource.Parse("""
|
||||
using System;
|
||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
|
||||
public sealed class GetModelAttribute : Attribute { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Rule
|
||||
{
|
||||
public interface IContextAware { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Extensions
|
||||
{
|
||||
public static class ContextAwareServiceExtensions
|
||||
{
|
||||
public static T GetModel<T>(this object contextAware) => default!;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||
|
||||
public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
|
||||
{
|
||||
[GetModel]
|
||||
private readonly IInventoryModel {|#0:_model|} = null!;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source.Source }
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(source.WithSpan(
|
||||
new DiagnosticResult("GF_ContextGet_003", DiagnosticSeverity.Error),
|
||||
"0")
|
||||
.WithArguments("_model"));
|
||||
|
||||
await test.RunAsync();
|
||||
Assert.Pass();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_For_Static_Explicit_GetModel_Field()
|
||||
{
|
||||
var source = MarkupTestSource.Parse("""
|
||||
using System;
|
||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||
|
||||
namespace GFramework.SourceGenerators.Abstractions.Rule
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false)]
|
||||
public sealed class GetModelAttribute : Attribute { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Rule
|
||||
{
|
||||
public interface IContextAware { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Extensions
|
||||
{
|
||||
public static class ContextAwareServiceExtensions
|
||||
{
|
||||
public static T GetModel<T>(this object contextAware) => default!;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||
|
||||
public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
|
||||
{
|
||||
[GetModel]
|
||||
private static IInventoryModel {|#0:_model|} = null!;
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source.Source }
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(source.WithSpan(
|
||||
new DiagnosticResult("GF_ContextGet_002", DiagnosticSeverity.Error),
|
||||
"0")
|
||||
.WithArguments("_model"));
|
||||
|
||||
await test.RunAsync();
|
||||
Assert.Pass();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Generates_Bindings_For_GetModels_Field_Assignable_From_IReadOnlyList()
|
||||
{
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
GF_ContextGet_004 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
||||
GF_ContextGet_005 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
||||
GF_ContextGet_006 | GFramework.SourceGenerators.rule | Error | ContextGetDiagnostics
|
||||
GF_ContextGet_007 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
|
||||
GF_ContextGet_008 | GFramework.SourceGenerators.rule | Warning | ContextGetDiagnostics
|
||||
GF_Priority_001 | GFramework.Priority | Error | PriorityDiagnostic
|
||||
GF_Priority_002 | GFramework.Priority | Warning | PriorityDiagnostic
|
||||
GF_Priority_003 | GFramework.Priority | Error | PriorityDiagnostic
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using GFramework.SourceGenerators.Common.Constants;
|
||||
|
||||
namespace GFramework.SourceGenerators.Diagnostics;
|
||||
|
||||
@ -7,6 +7,8 @@ namespace GFramework.SourceGenerators.Diagnostics;
|
||||
/// </summary>
|
||||
public static class ContextGetDiagnostics
|
||||
{
|
||||
private const string SourceGeneratorsRuleCategory = $"{PathContests.SourceGeneratorsPath}.Rule";
|
||||
|
||||
/// <summary>
|
||||
/// 不支持在嵌套类中生成注入代码。
|
||||
/// </summary>
|
||||
@ -14,7 +16,7 @@ public static class ContextGetDiagnostics
|
||||
"GF_ContextGet_001",
|
||||
"Context Get injection does not support nested classes",
|
||||
"Class '{0}' cannot use context Get injection inside a nested type",
|
||||
"GFramework.SourceGenerators.Rule",
|
||||
SourceGeneratorsRuleCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
|
||||
@ -25,7 +27,7 @@ public static class ContextGetDiagnostics
|
||||
"GF_ContextGet_002",
|
||||
"Static field is not supported for context Get injection",
|
||||
"Field '{0}' cannot be static when using generated context Get injection",
|
||||
"GFramework.SourceGenerators.Rule",
|
||||
SourceGeneratorsRuleCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
|
||||
@ -36,10 +38,32 @@ public static class ContextGetDiagnostics
|
||||
"GF_ContextGet_003",
|
||||
"Readonly field is not supported for context Get injection",
|
||||
"Field '{0}' cannot be readonly when using generated context Get injection",
|
||||
"GFramework.SourceGenerators.Rule",
|
||||
SourceGeneratorsRuleCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <c>[GetAll]</c> 时,静态字段会被跳过且不会生成注入赋值。
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor GetAllStaticFieldSkipped = new(
|
||||
"GF_ContextGet_007",
|
||||
"Static field will be skipped by [GetAll] context Get injection",
|
||||
"Field '{0}' is static and will be skipped by [GetAll] context Get injection generation",
|
||||
SourceGeneratorsRuleCategory,
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
/// <summary>
|
||||
/// 使用 <c>[GetAll]</c> 时,只读字段会被跳过且不会生成注入赋值。
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor GetAllReadOnlyFieldSkipped = new(
|
||||
"GF_ContextGet_008",
|
||||
"Readonly field will be skipped by [GetAll] context Get injection",
|
||||
"Field '{0}' is readonly and will be skipped by [GetAll] context Get injection generation",
|
||||
SourceGeneratorsRuleCategory,
|
||||
DiagnosticSeverity.Warning,
|
||||
true);
|
||||
|
||||
/// <summary>
|
||||
/// 字段类型与注入特性不匹配。
|
||||
/// </summary>
|
||||
@ -47,7 +71,7 @@ public static class ContextGetDiagnostics
|
||||
"GF_ContextGet_004",
|
||||
"Field type is not valid for the selected context Get attribute",
|
||||
"Field '{0}' type '{1}' is not valid for [{2}]",
|
||||
"GFramework.SourceGenerators.Rule",
|
||||
SourceGeneratorsRuleCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
|
||||
@ -58,7 +82,7 @@ public static class ContextGetDiagnostics
|
||||
"GF_ContextGet_005",
|
||||
"Context-aware type is required",
|
||||
"Class '{0}' must be context-aware to use generated context Get injection",
|
||||
"GFramework.SourceGenerators.Rule",
|
||||
SourceGeneratorsRuleCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
|
||||
@ -69,7 +93,7 @@ public static class ContextGetDiagnostics
|
||||
"GF_ContextGet_006",
|
||||
"Multiple context Get attributes are not supported on the same field",
|
||||
"Field '{0}' cannot declare multiple generated context Get attributes",
|
||||
"GFramework.SourceGenerators.Rule",
|
||||
SourceGeneratorsRuleCategory,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
}
|
||||
@ -16,5 +16,8 @@ global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
global using System.Collections.Immutable;
|
||||
global using Microsoft.CodeAnalysis;
|
||||
global using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
global using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
global using Microsoft.CodeAnalysis.CSharp;
|
||||
global using Microsoft.CodeAnalysis.Text;
|
||||
@ -47,3 +47,6 @@ public partial class InventoryPanel
|
||||
|
||||
`Service` 和 `Services` 绑定不会在 `[GetAll]` 下自动推断。对于普通引用类型字段,请显式使用 `[GetService]` 或
|
||||
`[GetServices]`,避免将非上下文服务字段误判为服务依赖。
|
||||
|
||||
`[GetAll]` 会跳过 `const`、`static` 和 `readonly` 字段。若某个字段本来会被 `[GetAll]` 推断为
|
||||
`Model`、`System` 或 `Utility` 绑定,但因为是不可赋值的 `static` 或 `readonly` 字段而被跳过,生成器会发出警告提示该字段不会参与生成。
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using GFramework.SourceGenerators.Common.Constants;
|
||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
||||
@ -299,23 +298,29 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
|
||||
if (explicitFields.Contains(field))
|
||||
continue;
|
||||
|
||||
if (!CanInferBinding(context, field))
|
||||
// Const fields are compile-time constants, so [GetAll] should skip them explicitly instead of relying on
|
||||
// type inference to fall through implicitly.
|
||||
if (field.IsConst)
|
||||
continue;
|
||||
|
||||
// Infer the target first so [GetAll] only warns for fields it would otherwise bind.
|
||||
if (!TryCreateInferredBinding(field, symbols, out var binding))
|
||||
continue;
|
||||
|
||||
if (!CanApplyInferredBinding(context, field))
|
||||
continue;
|
||||
|
||||
bindings.Add(binding);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CanInferBinding(SourceProductionContext context, IFieldSymbol field)
|
||||
private static bool CanApplyInferredBinding(SourceProductionContext context, IFieldSymbol field)
|
||||
{
|
||||
if (field.IsStatic)
|
||||
{
|
||||
ReportFieldDiagnostic(
|
||||
context,
|
||||
ContextGetDiagnostics.StaticFieldNotSupported,
|
||||
ContextGetDiagnostics.GetAllStaticFieldSkipped,
|
||||
field);
|
||||
return false;
|
||||
}
|
||||
@ -325,7 +330,7 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
|
||||
|
||||
ReportFieldDiagnostic(
|
||||
context,
|
||||
ContextGetDiagnostics.ReadOnlyFieldNotSupported,
|
||||
ContextGetDiagnostics.GetAllReadOnlyFieldSkipped,
|
||||
field);
|
||||
return false;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user