1

给定一个工作的源代码生成器和一个用于该生成器的工作测试项目。

发电机

<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.csobj/目录中看到一些。事实并非如此。

那么也许生成器没有打包在 nuget 包中?如您所见,分析仪正在打包。也许我也需要IncludeBuildOutput?不,也不工作。

现在我的问题是为什么会这样?IIncrementalGenerator与 相比,导入项目时是否有一些特定的东西,一些特定的属性,我需要注意ISourceGenerator,因为在项目中使用 anISourceGenerator的工作方式完全相同?

还有什么我可以尝试让增量源生成器工作的,还是我应该恢复使用常规源生成器?

对好文章的引用也有帮助,因为实际上找不到文档。工作时我参考了大部分Andrew Lock的源码生成器相关文章,特别是这一篇

我在一周前测试了这个 mit net 6.0.101 和 6.0.2xx 的构建。

4

0 回答 0