Incremental source generator not called in production build context
Asked Answered
P

0

3

Given a working source generator and a working test project for the generator.

Generator

<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));
        }
    }
}

Test project


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));
}

The test runs without error, the types and methods are generated correctly. But I absolutely hate writing tests in plain text, plus executing the tests like so doesnt yield test coverage or unit test cases, so I want to write a production test for the source generator. As per usual I create a .Run.Tests project and add the Rustic.DataEnumGen nuget project as an analyzer. Like so

<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));
        }
    }
}

This is the exact same code as in the previous test, but wrapped in a TestFixture instead of a console application. So I build the project with the analyzer, so that the DataEnumAttribute generated and then add the code above. But the code does not compile, because the type DataEnum or DataEnumAttribute does not exist.

First I thought that I needed to (I) ReferenceOutputAssembly, but that didn't change anything either, then I tried combinations of removing OutputItemType="Analyzer" and hoping that would result in the analyzer being called; nothing helped.

I conclude, that in this example the imported source generator, the very same that works in the first test case with the plain text compilation, is not executed before building the project, because if that was the case then type that is always added by the generator would be available in the project, and I would see some Rusic.*.g.cs in the obj/ directory. That is not the case.

So maybe the generator is not packed in the nuget package? As you can see the analyzer is being packed. Maybe I need to IncludeBuildOutput aswell? Nope not working either.

Now my question is why that is the case? Is there some specific thing, some specific attribute, I need to pay attention to when importing IIncrementalGenerator into a project compared to ISourceGenerator, because using an ISourceGenerator in a project works in the exact same way?

Is there anything else I might try to get the incremental source generator to work, or should I just revert to using a regular source generator?

References to good articles help as well, because there is effectively no doc to be found. When working I referenced most of Andrew Lock`s source generator related articles, specifically this one.

I tested this mit net 6.0.101 and a build of 6.0.2xx from like a week ago.

Pelagia answered 11/2, 2022 at 11:2 Comment(1)
Note I recently faced this kind of problem, i.e.: the generator just doesn't run and it just turned out Visual Studio version 2022 must be 17.1.0 at minimum as incremental source generator support is very recent.Marketa

© 2022 - 2025 — McMap. All rights reserved.