diff --git a/GFramework.SourceGenerators.Tests/Core/MarkupTestSource.cs b/GFramework.SourceGenerators.Tests/Core/MarkupTestSource.cs new file mode 100644 index 0000000..8e97991 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Core/MarkupTestSource.cs @@ -0,0 +1,120 @@ +using System.Text; + +namespace GFramework.SourceGenerators.Tests.Core; + +/// +/// 为源生成器测试提供轻量的源码标记解析能力。 +/// +public sealed class MarkupTestSource +{ + private readonly SourceText _sourceText; + private readonly IReadOnlyDictionary _spans; + + private MarkupTestSource( + string source, + SourceText sourceText, + IReadOnlyDictionary spans) + { + Source = source; + _sourceText = sourceText; + _spans = spans; + } + + /// + /// 获取移除标记后的源码文本。 + /// + public string Source { get; } + + /// + /// 解析形如 {|#0:identifier|} 的单层标记,并保留去标记后的源码。 + /// + /// 包含测试标记的源码。 + /// 可用于测试输入和诊断定位的解析结果。 + /// 标记格式不合法,或存在重复标记编号时抛出。 + public static MarkupTestSource Parse(string markupSource) + { + var builder = new StringBuilder(markupSource.Length); + var spans = new Dictionary(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); + } + + /// + /// 将标记位置应用到诊断断言,避免测试依赖硬编码行列号。 + /// + /// 要补全定位信息的诊断断言。 + /// 标记编号。 + /// 包含定位信息的诊断断言。 + /// 指定标记不存在时抛出。 + 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] == '}'; + } +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs index 30d165a..1bea458 100644 --- a/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs @@ -463,63 +463,63 @@ public class ContextGetGeneratorTests [Test] public async Task Warns_And_Skips_Readonly_Inferred_Field_For_GetAll_Class() { - 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.Class, Inherited = false)] - public sealed class GetAllAttribute : Attribute { } - } + 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.Abstractions.Rule + { + public interface IContextAware { } + } - namespace GFramework.Core.Rule - { - public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.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.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(this object contextAware) => default!; - public static T GetSystem(this object contextAware) => default!; - } - } + namespace GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + public static T GetSystem(this object contextAware) => default!; + } + } - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { } + 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 _model = null!; - private ICombatSystem _system = null!; - } - } - """; + [GetAll] + public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase + { + private readonly IInventoryModel {|#0:_model|} = null!; + private ICombatSystem _system = null!; + } + } + """); const string expected = """ // @@ -543,7 +543,7 @@ public class ContextGetGeneratorTests { TestState = { - Sources = { source }, + Sources = { source.Source }, GeneratedSources = { (typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected) @@ -552,8 +552,9 @@ public class ContextGetGeneratorTests DisabledDiagnostics = { "GF_Common_Trace_001" } }; - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_008", DiagnosticSeverity.Warning) - .WithSpan(52, 42, 52, 48) + test.ExpectedDiagnostics.Add(source.WithSpan( + new DiagnosticResult("GF_ContextGet_008", DiagnosticSeverity.Warning), + "0") .WithArguments("_model")); await test.RunAsync(); @@ -563,63 +564,63 @@ public class ContextGetGeneratorTests [Test] public async Task Warns_And_Skips_Static_Inferred_Field_For_GetAll_Class() { - 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.Class, Inherited = false)] - public sealed class GetAllAttribute : Attribute { } - } + 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.Abstractions.Rule + { + public interface IContextAware { } + } - namespace GFramework.Core.Rule - { - public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.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.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(this object contextAware) => default!; - public static T GetSystem(this object contextAware) => default!; - } - } + namespace GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(this object contextAware) => default!; + public static T GetSystem(this object contextAware) => default!; + } + } - namespace TestApp - { - public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { } - public interface ICombatSystem : GFramework.Core.Abstractions.Systems.ISystem { } + 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 _model = null!; - private ICombatSystem _system = null!; - } - } - """; + [GetAll] + public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase + { + private static IInventoryModel {|#0:_model|} = null!; + private ICombatSystem _system = null!; + } + } + """); const string expected = """ // @@ -643,7 +644,7 @@ public class ContextGetGeneratorTests { TestState = { - Sources = { source }, + Sources = { source.Source }, GeneratedSources = { (typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected) @@ -652,8 +653,9 @@ public class ContextGetGeneratorTests DisabledDiagnostics = { "GF_Common_Trace_001" } }; - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_007", DiagnosticSeverity.Warning) - .WithSpan(52, 40, 52, 46) + test.ExpectedDiagnostics.Add(source.WithSpan( + new DiagnosticResult("GF_ContextGet_007", DiagnosticSeverity.Warning), + "0") .WithArguments("_model")); await test.RunAsync(); @@ -832,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(this object contextAware) => default!; - } - } + namespace GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(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 { 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(); @@ -897,68 +900,69 @@ 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 GetModels(this object contextAware) => default!; - } - } + namespace GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static IReadOnlyList GetModels(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 _models = new(); - } - } - """; + public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware + { + [GetModels] + private List {|#0:_models|} = new(); + } + } + """); var test = new CSharpSourceGeneratorTest { 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", "GetModels")); await test.RunAsync(); @@ -968,67 +972,68 @@ public class ContextGetGeneratorTests [Test] public async Task Reports_Diagnostic_For_Readonly_Explicit_GetModel_Field() { - 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.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 T GetModel(this object contextAware) => default!; - } - } + namespace GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(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 - { - [GetModel] - private readonly IInventoryModel _model = null!; - } - } - """; + public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware + { + [GetModel] + private readonly IInventoryModel {|#0:_model|} = null!; + } + } + """); var test = new CSharpSourceGeneratorTest { TestState = { - Sources = { source } + Sources = { source.Source } }, DisabledDiagnostics = { "GF_Common_Trace_001" } }; - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_003", DiagnosticSeverity.Error) - .WithSpan(45, 42, 45, 48) + test.ExpectedDiagnostics.Add(source.WithSpan( + new DiagnosticResult("GF_ContextGet_003", DiagnosticSeverity.Error), + "0") .WithArguments("_model")); await test.RunAsync(); @@ -1038,67 +1043,68 @@ public class ContextGetGeneratorTests [Test] public async Task Reports_Diagnostic_For_Static_Explicit_GetModel_Field() { - 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.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 T GetModel(this object contextAware) => default!; - } - } + namespace GFramework.Core.Extensions + { + public static class ContextAwareServiceExtensions + { + public static T GetModel(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 - { - [GetModel] - private static IInventoryModel _model = null!; - } - } - """; + public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware + { + [GetModel] + private static IInventoryModel {|#0:_model|} = null!; + } + } + """); var test = new CSharpSourceGeneratorTest { TestState = { - Sources = { source } + Sources = { source.Source } }, DisabledDiagnostics = { "GF_Common_Trace_001" } }; - test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_002", DiagnosticSeverity.Error) - .WithSpan(45, 40, 45, 46) + test.ExpectedDiagnostics.Add(source.WithSpan( + new DiagnosticResult("GF_ContextGet_002", DiagnosticSeverity.Error), + "0") .WithArguments("_model")); await test.RunAsync(); diff --git a/GFramework.SourceGenerators/README.md b/GFramework.SourceGenerators/README.md index 6578ddb..7ed6db8 100644 --- a/GFramework.SourceGenerators/README.md +++ b/GFramework.SourceGenerators/README.md @@ -49,4 +49,4 @@ public partial class InventoryPanel `[GetServices]`,避免将非上下文服务字段误判为服务依赖。 `[GetAll]` 会跳过 `const`、`static` 和 `readonly` 字段。若某个字段本来会被 `[GetAll]` 推断为 -`Model`、`System` 或 `Utility` 绑定,但因为字段不可赋值而被跳过,生成器会发出警告提示该字段不会参与生成。 +`Model`、`System` 或 `Utility` 绑定,但因为是不可赋值的 `static` 或 `readonly` 字段而被跳过,生成器会发出警告提示该字段不会参与生成。 diff --git a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs index 6d47253..83aaf0e 100644 --- a/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs +++ b/GFramework.SourceGenerators/Rule/ContextGetGenerator.cs @@ -1,7 +1,5 @@ -using System.Text; using GFramework.SourceGenerators.Common.Constants; using GFramework.SourceGenerators.Common.Diagnostics; -using GFramework.SourceGenerators.Common.Extensions; using GFramework.SourceGenerators.Common.Info; using GFramework.SourceGenerators.Diagnostics; @@ -298,6 +296,11 @@ public sealed class ContextGetGenerator : IIncrementalGenerator if (explicitFields.Contains(field)) continue; + // 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;