给定一个工作的源代码生成器和一个用于该生成器的工作测试项目。
发电机
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
[...]
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />
</ItemGroup>
<ItemGroup Condition="!$(DefineConstants.Contains('NET5_0_OR_GREATER'))">
<PackageReference Include="System.Memory" Version="4.5.4" />
</ItemGroup>
[...]
</Project>
namespace Rustic.DataEnumGen;
[Generator(LanguageNames.CSharp)]
[CLSCompliant(false)]
public class DataEnumGen : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(ctx => ctx.AddSource($"{GenInfo.DataEnumSymbol}.g.cs", SourceText.From(GenInfo.DataEnumSyntax, Encoding.UTF8)));
var enumDecls = context.SyntaxProvider.CreateSyntaxProvider(
static (s, _) => IsEnumDecl(s),
static (ctx, _) => CollectTreeInfo(ctx))
.Where(static m => m.HasValue)
.Select(static (m, _) => m!.Value);
var compilationEnumDeclUnion = context.CompilationProvider.Combine(enumDecls.Collect());
context.RegisterSourceOutput(compilationEnumDeclUnion, static (spc, source) => Generate(source.Right, spc));
}
private static bool IsEnumDecl(SyntaxNode node)
{
return node is EnumDeclarationSyntax;
}
private static GenInfo? CollectTreeInfo(GeneratorSyntaxContext context)
{
[...]
}
private static EnumDeclInfo CollectEnumDeclInfo(GeneratorSyntaxContext context, EnumMemberDeclarationSyntax memberDecl)
{
[...]
}
private static void Generate(ImmutableArray<GenInfo> members, SourceProductionContext context)
{
if (members.IsDefaultOrEmpty)
{
return;
}
foreach (var info in members.Distinct())
{
SrcBuilder text = new(2048);
GenInfo.Generate(text, in info);
context.AddSource($"{info.EnumName}Value.g.cs", SourceText.From(text.ToString(), Encoding.UTF8));
}
}
}
测试项目
namespace Rustic.DataEnumGen.Tests;
[TestFixture]
public class GeneratorTests
{
private readonly StreamWriter _writer;
public GeneratorTests()
{
_writer = new StreamWriter($"GeneratorTests-{typeof(string).Assembly.ImageRuntimeVersion}.log", true);
_writer.AutoFlush = true;
Logger = new Logger(nameof(GeneratorTests), InternalTraceLevel.Debug, _writer);
}
~GeneratorTests()
{
_writer.Dispose();
}
internal Logger Logger { get; }
[Test]
public void SimpleGeneratorTest()
{
// Create the 'input' compilation that the generator will act on
Compilation inputCompilation = CreateCompilation(@"
using System;
using System.ComponentModel;
using Rustic;
namespace Rustic.DataEnumGen.Tests.TestAssembly
{
using static DummyValue;
public enum Dummy : byte
{
[Description(""The default value."")]
Default = 0,
[Rustic.DataEnum(typeof((int, int)))]
Minimum = 1,
[Rustic.DataEnum(typeof((long, long)))]
Maximum = 2,
}
public enum NoAttr
{
[Description(""This is a description."")]
This,
Is,
Sparta,
}
[Flags]
public enum NoFlags : byte
{
Flag = 1 << 0,
Enums = 1 << 1,
Are = 1 << 2,
Not = 1 << 3,
Supported = 1 << 4,
}
public static class Program
{
public static void Main()
{
DummyValue empty = default!;
DummyValue default = Default();
DummyValue min = Minimum((12, 43));
DummyValue min = Maximum((12, 43));
}
}
}
");
const int TEST_SOURCES_LEN = 1;
const int GEN_SOURCES_LEN = 3; // Attribute + Dummy + NoAttr
DataEnumGen generator = new();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var outputCompilation, out var diagnostics);
[...Validation...]
}
private void Logging(Compilation comp, ImmutableArray<Diagnostic> diagnostics)
{
foreach (var diag in diagnostics)
{
Logger.Debug("Initial diagnostics {0}", diag.ToString());
}
foreach (var tree in comp.SyntaxTrees)
{
Logger.Debug("SyntaxTree\nName=\"{0}\",\nText=\"{1}\"", tree.FilePath, tree.ToString());
}
var d = comp.GetDiagnostics();
foreach (var diag in d)
{
Logger.Debug("Diagnostics {0}", diag.ToString());
}
}
private static Compilation CreateCompilation(string source)
=> CSharpCompilation.Create("compilation",
new[] { CSharpSyntaxTree.ParseText(source) },
new[]
{
MetadataReference.CreateFromFile(typeof(System.String).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.ComponentModel.DescriptionAttribute).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(ReadOnlySpan<char>).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.Collections.Generic.List<char>).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.MethodImplAttribute).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.Runtime.Serialization.ISerializable).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.Runtime.InteropServices.StructLayoutAttribute).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(@"C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\6.0.1\System.Runtime.dll"),
},
new CSharpCompilationOptions(OutputKind.ConsoleApplication));
}
测试运行没有错误,类型和方法生成正确。但是我绝对讨厌用纯文本编写测试,加上执行这样的测试不会产生测试覆盖率或单元测试用例,所以我想为源生成器编写一个生产测试。按照惯例,我创建了一个.Run.Tests
项目并将Rustic.DataEnumGen
nuget项目添加为分析器。像这样
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net48;net50;net60</TargetFrameworks>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AltCover" Version="8.2.835" />
<PackageReference Include="bogus" Version="33.0.2" />
<PackageReference Include="fluentassertions" Version="5.10.3" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Rustic.DataEnumGen" Version="0.5.0" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
using System;
using System.ComponentModel;
using NUnit.Framework;
using Rustic;
namespace Rustic.DataEnumGen.Run.Tests
{
using static DummyValue;
public enum Dummy : byte
{
[Description("The default value.")]
Default = 0,
[DataEnum(typeof((int, int)))]
Minimum = 1,
[DataEnum(typeof((long, long)))]
Maximum = 2,
}
public enum NoAttr
{
[Description("This is a description.")]
This,
Is,
Sparta,
}
[Flags]
public enum NoFlags : byte
{
Flag = 1 << 0,
Enums = 1 << 1,
Are = 1 << 2,
Not = 1 << 3,
Supported = 1 << 4,
}
[TestFixture]
public static class DataEnumRunTests
{
[Test]
public static void TestFactory()
{
DummyValue empty = default!;
DummyValue default = Default();
DummyValue min = Minimum((12, 43));
DummyValue min = Maximum((12, 43));
}
[Test]
public static void TestImplicitEnumCast()
{
Dummy default = Default();
Dummy min = Minimum((12, 43));
Dummy min = Maximum((12, 43));
}
}
}
这与前一个测试中的代码完全相同,但包装在 aTestFixture
而不是控制台应用程序中。所以我用分析器构建项目,这样DataEnumAttribute
生成然后添加上面的代码。但是代码不能编译,因为类型DataEnum
还是DataEnumAttribute
不存在的。
首先我认为我需要 (I) ReferenceOutputAssembly
,但这也没有改变任何东西,然后我尝试了删除OutputItemType="Analyzer"
和希望这会导致调用分析器的组合;没有任何帮助。
我得出结论,在这个例子中,导入的源代码生成器,在第一个测试用例中使用纯文本编译时工作的相同,在构建项目之前没有执行,因为如果是这种情况,那么类型总是由生成器将在项目中可用,我会Rusic.*.g.cs
在obj/
目录中看到一些。事实并非如此。
那么也许生成器没有打包在 nuget 包中?如您所见,分析仪正在打包。也许我也需要IncludeBuildOutput
?不,也不工作。
现在我的问题是为什么会这样?IIncrementalGenerator
与 相比,导入项目时是否有一些特定的东西,一些特定的属性,我需要注意ISourceGenerator
,因为在项目中使用 anISourceGenerator
的工作方式完全相同?
还有什么我可以尝试让增量源生成器工作的,还是我应该恢复使用常规源生成器?
对好文章的引用也有帮助,因为实际上找不到文档。工作时我参考了大部分Andrew Lock的源码生成器相关文章,特别是这一篇。
我在一周前测试了这个 mit net 6.0.101 和 6.0.2xx 的构建。