mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-31 18:39:00 +08:00
feat(generator): 添加对 const 字段的显式跳过支持
- 在 ContextGetGenerator 中添加对 const 字段的显式检查和跳过逻辑 - 更新文档说明 const、static 和 readonly 字段的处理方式 - 重构测试代码使用 MarkupTestSource 解析器进行更精确的诊断测试 - 添加新的 MarkupTestSource 类用于源码标记解析和诊断定位
This commit is contained in:
parent
2ae16b22eb
commit
da34b2fa2a
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] == '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -463,7 +463,7 @@ public class ContextGetGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Warns_And_Skips_Readonly_Inferred_Field_For_GetAll_Class()
|
public async Task Warns_And_Skips_Readonly_Inferred_Field_For_GetAll_Class()
|
||||||
{
|
{
|
||||||
var source = """
|
var source = MarkupTestSource.Parse("""
|
||||||
using System;
|
using System;
|
||||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
@ -515,11 +515,11 @@ public class ContextGetGeneratorTests
|
|||||||
[GetAll]
|
[GetAll]
|
||||||
public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
|
public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
|
||||||
{
|
{
|
||||||
private readonly IInventoryModel _model = null!;
|
private readonly IInventoryModel {|#0:_model|} = null!;
|
||||||
private ICombatSystem _system = null!;
|
private ICombatSystem _system = null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""";
|
""");
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -543,7 +543,7 @@ public class ContextGetGeneratorTests
|
|||||||
{
|
{
|
||||||
TestState =
|
TestState =
|
||||||
{
|
{
|
||||||
Sources = { source },
|
Sources = { source.Source },
|
||||||
GeneratedSources =
|
GeneratedSources =
|
||||||
{
|
{
|
||||||
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
|
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
|
||||||
@ -552,8 +552,9 @@ public class ContextGetGeneratorTests
|
|||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
};
|
};
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_008", DiagnosticSeverity.Warning)
|
test.ExpectedDiagnostics.Add(source.WithSpan(
|
||||||
.WithSpan(52, 42, 52, 48)
|
new DiagnosticResult("GF_ContextGet_008", DiagnosticSeverity.Warning),
|
||||||
|
"0")
|
||||||
.WithArguments("_model"));
|
.WithArguments("_model"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync();
|
||||||
@ -563,7 +564,7 @@ public class ContextGetGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Warns_And_Skips_Static_Inferred_Field_For_GetAll_Class()
|
public async Task Warns_And_Skips_Static_Inferred_Field_For_GetAll_Class()
|
||||||
{
|
{
|
||||||
var source = """
|
var source = MarkupTestSource.Parse("""
|
||||||
using System;
|
using System;
|
||||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
@ -615,11 +616,11 @@ public class ContextGetGeneratorTests
|
|||||||
[GetAll]
|
[GetAll]
|
||||||
public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
|
public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
|
||||||
{
|
{
|
||||||
private static IInventoryModel _model = null!;
|
private static IInventoryModel {|#0:_model|} = null!;
|
||||||
private ICombatSystem _system = null!;
|
private ICombatSystem _system = null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""";
|
""");
|
||||||
|
|
||||||
const string expected = """
|
const string expected = """
|
||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
@ -643,7 +644,7 @@ public class ContextGetGeneratorTests
|
|||||||
{
|
{
|
||||||
TestState =
|
TestState =
|
||||||
{
|
{
|
||||||
Sources = { source },
|
Sources = { source.Source },
|
||||||
GeneratedSources =
|
GeneratedSources =
|
||||||
{
|
{
|
||||||
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
|
(typeof(ContextGetGenerator), "TestApp_BattlePanel.ContextGet.g.cs", expected)
|
||||||
@ -652,8 +653,9 @@ public class ContextGetGeneratorTests
|
|||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
};
|
};
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_007", DiagnosticSeverity.Warning)
|
test.ExpectedDiagnostics.Add(source.WithSpan(
|
||||||
.WithSpan(52, 40, 52, 46)
|
new DiagnosticResult("GF_ContextGet_007", DiagnosticSeverity.Warning),
|
||||||
|
"0")
|
||||||
.WithArguments("_model"));
|
.WithArguments("_model"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync();
|
||||||
@ -832,7 +834,7 @@ public class ContextGetGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Diagnostic_When_Class_Is_Not_ContextAware()
|
public async Task Reports_Diagnostic_When_Class_Is_Not_ContextAware()
|
||||||
{
|
{
|
||||||
var source = """
|
var source = MarkupTestSource.Parse("""
|
||||||
using System;
|
using System;
|
||||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
@ -872,22 +874,23 @@ public class ContextGetGeneratorTests
|
|||||||
public partial class InventoryPanel
|
public partial class InventoryPanel
|
||||||
{
|
{
|
||||||
[GetModel]
|
[GetModel]
|
||||||
private IInventoryModel _model = null!;
|
private IInventoryModel {|#0:_model|} = null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""";
|
""");
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||||
{
|
{
|
||||||
TestState =
|
TestState =
|
||||||
{
|
{
|
||||||
Sources = { source }
|
Sources = { source.Source }
|
||||||
},
|
},
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
};
|
};
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_005", DiagnosticSeverity.Error)
|
test.ExpectedDiagnostics.Add(source.WithSpan(
|
||||||
.WithSpan(40, 33, 40, 39)
|
new DiagnosticResult("GF_ContextGet_005", DiagnosticSeverity.Error),
|
||||||
|
"0")
|
||||||
.WithArguments("InventoryPanel"));
|
.WithArguments("InventoryPanel"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync();
|
||||||
@ -897,7 +900,7 @@ public class ContextGetGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Diagnostic_When_GetModels_Field_Is_Not_IReadOnlyList()
|
public async Task Reports_Diagnostic_When_GetModels_Field_Is_Not_IReadOnlyList()
|
||||||
{
|
{
|
||||||
var source = """
|
var source = MarkupTestSource.Parse("""
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
@ -943,22 +946,23 @@ public class ContextGetGeneratorTests
|
|||||||
public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
|
public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
|
||||||
{
|
{
|
||||||
[GetModels]
|
[GetModels]
|
||||||
private List<IInventoryModel> _models = new();
|
private List<IInventoryModel> {|#0:_models|} = new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""";
|
""");
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||||
{
|
{
|
||||||
TestState =
|
TestState =
|
||||||
{
|
{
|
||||||
Sources = { source }
|
Sources = { source.Source }
|
||||||
},
|
},
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
};
|
};
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_004", DiagnosticSeverity.Error)
|
test.ExpectedDiagnostics.Add(source.WithSpan(
|
||||||
.WithSpan(46, 39, 46, 46)
|
new DiagnosticResult("GF_ContextGet_004", DiagnosticSeverity.Error),
|
||||||
|
"0")
|
||||||
.WithArguments("_models", "System.Collections.Generic.List<TestApp.IInventoryModel>", "GetModels"));
|
.WithArguments("_models", "System.Collections.Generic.List<TestApp.IInventoryModel>", "GetModels"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync();
|
||||||
@ -968,7 +972,7 @@ public class ContextGetGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Diagnostic_For_Readonly_Explicit_GetModel_Field()
|
public async Task Reports_Diagnostic_For_Readonly_Explicit_GetModel_Field()
|
||||||
{
|
{
|
||||||
var source = """
|
var source = MarkupTestSource.Parse("""
|
||||||
using System;
|
using System;
|
||||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
@ -1013,22 +1017,23 @@ public class ContextGetGeneratorTests
|
|||||||
public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
|
public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
|
||||||
{
|
{
|
||||||
[GetModel]
|
[GetModel]
|
||||||
private readonly IInventoryModel _model = null!;
|
private readonly IInventoryModel {|#0:_model|} = null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""";
|
""");
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||||
{
|
{
|
||||||
TestState =
|
TestState =
|
||||||
{
|
{
|
||||||
Sources = { source }
|
Sources = { source.Source }
|
||||||
},
|
},
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
};
|
};
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_003", DiagnosticSeverity.Error)
|
test.ExpectedDiagnostics.Add(source.WithSpan(
|
||||||
.WithSpan(45, 42, 45, 48)
|
new DiagnosticResult("GF_ContextGet_003", DiagnosticSeverity.Error),
|
||||||
|
"0")
|
||||||
.WithArguments("_model"));
|
.WithArguments("_model"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync();
|
||||||
@ -1038,7 +1043,7 @@ public class ContextGetGeneratorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task Reports_Diagnostic_For_Static_Explicit_GetModel_Field()
|
public async Task Reports_Diagnostic_For_Static_Explicit_GetModel_Field()
|
||||||
{
|
{
|
||||||
var source = """
|
var source = MarkupTestSource.Parse("""
|
||||||
using System;
|
using System;
|
||||||
using GFramework.SourceGenerators.Abstractions.Rule;
|
using GFramework.SourceGenerators.Abstractions.Rule;
|
||||||
|
|
||||||
@ -1083,22 +1088,23 @@ public class ContextGetGeneratorTests
|
|||||||
public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
|
public partial class InventoryPanel : GFramework.Core.Abstractions.Rule.IContextAware
|
||||||
{
|
{
|
||||||
[GetModel]
|
[GetModel]
|
||||||
private static IInventoryModel _model = null!;
|
private static IInventoryModel {|#0:_model|} = null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""";
|
""");
|
||||||
|
|
||||||
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||||
{
|
{
|
||||||
TestState =
|
TestState =
|
||||||
{
|
{
|
||||||
Sources = { source }
|
Sources = { source.Source }
|
||||||
},
|
},
|
||||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||||
};
|
};
|
||||||
|
|
||||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_ContextGet_002", DiagnosticSeverity.Error)
|
test.ExpectedDiagnostics.Add(source.WithSpan(
|
||||||
.WithSpan(45, 40, 45, 46)
|
new DiagnosticResult("GF_ContextGet_002", DiagnosticSeverity.Error),
|
||||||
|
"0")
|
||||||
.WithArguments("_model"));
|
.WithArguments("_model"));
|
||||||
|
|
||||||
await test.RunAsync();
|
await test.RunAsync();
|
||||||
|
|||||||
@ -49,4 +49,4 @@ public partial class InventoryPanel
|
|||||||
`[GetServices]`,避免将非上下文服务字段误判为服务依赖。
|
`[GetServices]`,避免将非上下文服务字段误判为服务依赖。
|
||||||
|
|
||||||
`[GetAll]` 会跳过 `const`、`static` 和 `readonly` 字段。若某个字段本来会被 `[GetAll]` 推断为
|
`[GetAll]` 会跳过 `const`、`static` 和 `readonly` 字段。若某个字段本来会被 `[GetAll]` 推断为
|
||||||
`Model`、`System` 或 `Utility` 绑定,但因为字段不可赋值而被跳过,生成器会发出警告提示该字段不会参与生成。
|
`Model`、`System` 或 `Utility` 绑定,但因为是不可赋值的 `static` 或 `readonly` 字段而被跳过,生成器会发出警告提示该字段不会参与生成。
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
using System.Text;
|
|
||||||
using GFramework.SourceGenerators.Common.Constants;
|
using GFramework.SourceGenerators.Common.Constants;
|
||||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
using GFramework.SourceGenerators.Common.Diagnostics;
|
||||||
using GFramework.SourceGenerators.Common.Extensions;
|
|
||||||
using GFramework.SourceGenerators.Common.Info;
|
using GFramework.SourceGenerators.Common.Info;
|
||||||
using GFramework.SourceGenerators.Diagnostics;
|
using GFramework.SourceGenerators.Diagnostics;
|
||||||
|
|
||||||
@ -298,6 +296,11 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
|
|||||||
if (explicitFields.Contains(field))
|
if (explicitFields.Contains(field))
|
||||||
continue;
|
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.
|
// Infer the target first so [GetAll] only warns for fields it would otherwise bind.
|
||||||
if (!TryCreateInferredBinding(field, symbols, out var binding))
|
if (!TryCreateInferredBinding(field, symbols, out var binding))
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user