C# Source Generator not including results from Project Reference
Asked Answered
H

2

9

Edit3: At some point this just started working. No clue why. Maybe it was a VS bug that got fixed?

Edit2: Looking into the Analyzers node in solution explorer, I've discovered the source generator runs successfully when I first open the program, and then it stops and everything it generated goes away after only a few changes to my code.

immediately after opening solution:
> Analyzers
>> MySourceGenerators
>>> MySourceGenerators.NotifyPropertyChangesGenerator
>>>> _NotifyChangedClass_Notify.cs

after making any edits
> Analyzers
>> MySourceGenerators
>>> MySourceGenerators.NotifyPropertyChangesGenerator
>>>> This generator is not generating files.

Edit: After calling Debugger.Launch() as suggested by comments, I can confirm that the generator code is running, and the source text looks exactly like it's supposed to. But both the IDE and compiler still give errors as if the results aren't being included.

I'm trying to setup a source generator to be run from a local project reference but can't get it to actually run. My NUnit tests are passing, so I know the actual generation logic is fine, but a barebones test project both fails to compile and reports errors in Visual Studio. I'm using Visual Studio 2022 Preview 5.0, in case that matters.

<--generator.csproj-->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>10</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IncludeBuildOutpout>false</IncludeBuildOutpout>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
  </ItemGroup>

</Project>
<--testproject.csproj-->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MySourceGenerators\MySourceGenerators.csproj" 
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false"/>
  </ItemGroup>

</Project>
//generator.cs
[Generator]
public class NotifyPropertyChangesGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        var receiver = (NotifySyntaxReceiver)context.SyntaxReceiver!;

        if (receiver.Classes.Count > 0)
        {
            foreach (var c in receiver.Classes)
            {
                /* Generate the source */

                var source = SyntaxFactory.ParseCompilationUnit(builder.ToString())
                    .NormalizeWhitespace()
                    .GetText(Encoding.UTF8, Microsoft.CodeAnalysis.Text.SourceHashAlgorithm.Sha256);

                context.AddSource($"_{c.ClassDeclaration.Identifier.ValueText}_Notify", source);
            }
        }
    }

    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new NotifySyntaxReceiver());
    }

}

class NotifySyntaxReceiver : ISyntaxReceiver
{
    public List<NotifyClass> Classes { get; } = new();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {

        if (syntaxNode is ClassDeclarationSyntax cds)
        {
            /* Identify classes that need generation */
        }
    }
}
//testproject.cs
internal class NotifyChangedClass : INotifyPropertyChanged
{
    string n_Property;
}
Hernardo answered 29/10, 2021 at 5:49 Comment(0)
L
4

Unfortunately, even in current VS 2022 (version 17.0.5) the support for this feature is somewhat limited. As you noticed, the only moment when VS shows the correct state of code generated is after VS restart (not just solution load/unload, but a complete restart of the application). It isn't a problem when a generator is completed, and you only want to check what was generated, but it's pain during generator development. So I ended up with such an approach during development:

At the given generator during its debugging/development, we can add the output of generated files not only to the compilation context but to a temporary directory at the file system or only to the temporary directory until we are satisfied with the result.

To force the generator to run, we need to force rebuild the "testproject.csproj" project. I'd use the command line from the "testproject" project directory: 'dotnet clean; dotnet build'.

The generated files will end up in the output directory. We can watch them using VS Code, for example. VS Code won't block open files, but any other notepad with a non-blocking read will suffice. It is not an ideal solution, but at this moment it removes the primary pain of generator development: to see the actual result of code generation, we don't have to restart VS.

Example code draft for "generator.csproj" project:

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace Target.Generators
{
    [Generator]
    public class TargetGenerator : ISourceGenerator
    {
        private readonly ISourceBuilder _sourceBuilder;

        public TargetGenerator()
        {
            _sourceBuilder = new SourceBuilder();
        }

        public void Initialize(GeneratorInitializationContext context) =>
            _sourceBuilder.Initialize(context);

        public void Execute(GeneratorExecutionContext context)
        {
            // Uncomment these to lines to start debugging the generator in the separate VS instance
            //// Debugger.Launch();
            //// Debugger.Break();

            // comment/uncomment these lines to use ether 'default' or 'debug' source file writer
            ////var fileWriter = new DefaultSourceFileWriter(context);
            var fileWriter = new DebugSourceFileWriter(context, "C:\\code-gen");
            var fileBuilders = _sourceBuilder.Build(context);

            fileWriter.WriteFiles(fileBuilders);
        }
    }

    public interface ISourceBuilder
    {
        void Initialize(GeneratorInitializationContext context);
        IEnumerable<(string Filename, string Source)> Build(GeneratorExecutionContext context);
    }

    public class SourceBuilder : ISourceBuilder
    {
        public void Initialize(GeneratorInitializationContext context)
        {
        }

        public IEnumerable<(string Filename, string Source)> Build(GeneratorExecutionContext context)
        {
            // Here should be an actual source code generator implementation
            throw new NotImplementedException();
        }
    }

    public interface ISourceFileWriter
    {
        void WriteFiles(IEnumerable<(string Filename, string Source)> sourceFiles);
    }

    public class DefaultSourceFileWriter : ISourceFileWriter
    {
        private readonly GeneratorExecutionContext _context;

        public DefaultSourceFileWriter(GeneratorExecutionContext context)
        {
            _context = context;
        }

        public void WriteFiles(IEnumerable<(string Filename, string Source)> sourceFiles)
        {
            foreach (var sourceFile in sourceFiles)
            {
                AddFile(sourceFile);
            }
        }

        protected virtual void AddFile((string Filename, string Source) sourceFile)
        {
            _context.AddSource(
                sourceFile.Filename,
                SourceText.From(sourceFile.Source, Encoding.UTF8));
        }
    }

    public class DebugSourceFileWriter : DefaultSourceFileWriter
    {
        private readonly string _outputDirectoryRoot;

        public DebugSourceFileWriter(
            GeneratorExecutionContext context,
            string outputDirectoryRoot)
            : base(context)
        {
            _outputDirectoryRoot = outputDirectoryRoot;
        }

        protected override void AddFile((string Filename, string Source) sourceFile)
        {
            bool done = false;
            int cnt = 0;
            while (!done)
            {
                try
                {
                    var fullFileName = Path.Combine(_outputDirectoryRoot, sourceFile.Filename);
                    File.WriteAllText(fullFileName, sourceFile.Source, Encoding.UTF8);
                    done = true;
                }
                catch
                {
                    cnt++;
                    if (cnt > 5)
                    {
                        done = true;
                    }

                    Thread.Sleep(100);
                }
            }
        }
    }
}

Loculus answered 23/1, 2022 at 12:7 Comment(1)
CSharpGeneratorDriver would be the usual approach to debug and develop source generators. If you need AdditionalText you can get implementations like InMemoryAdditionalText from here: UnitTests SourceGenerationCas
D
1

Source generators target netstandard2.0, your project targets net6.0. That isn't an issue when you use source generators via PackageReference.

I think for ProjectReference to work in this case you need to add the SetTargetFramework meta data.

  <ItemGroup>
    <ProjectReference Include="..\MySourceGenerators\MySourceGenerators.csproj" 
                      OutputItemType="Analyzer"
                      SetTargetFramework="netstandard2.0"
                      ReferenceOutputAssembly="false"/>
  </ItemGroup>

That might work, sorry can't try right now.

Disposition answered 29/10, 2021 at 5:57 Comment(7)
this doesn't seem to have workedHernardo
@Hernardo Hmm, that's too bad. You could try adding System.Diagnostics.Debugger.Launch() inside your Initialize and/or Execute method to see if it is actually invoked.Disposition
@Hernardo Also, you didn't put the [Generator] attribute on your generator class. I think that is also need (as is the ISourceGenerator interface). So maybe it really isn't run - unit tests work differently and its possibly that this doesn't matter there.Disposition
good catch, but I'm afraid that didn't fix it either.Hernardo
Well, as mentioned in my edit, the debugger confirmed my code is running, the compiler/IDE just aren't getting the results for some reason.Hernardo
OK, then I'm at wit's end. Sorry. Appart from the stuff above, I cannot see any difference regarding your setup and the one I have, or have seen so far. As a last attempt, you could try packaging your generator as an nupkg and reference that (if only for sanity's purpose).Disposition
If you look in Solution Explorer, you can expand the analyzer node for your project consuming the generator, and that should let you see what the generator is generating. Does anything appear there?Outstanding

© 2022 - 2024 — McMap. All rights reserved.