3

I've created a source generator which barring some issues mostly seems to work.

I tested it during development using a test project with a project reference to the generator.

I've now packaged it up into a nuget package and am trying to use it in a different project with that but I'm getting this not very helpful warning:

CSC : warning CS8032: An instance of analyzer SuperFluid.Internal.SourceGenerators.FluidApiSourceGenerator cannot be created from /home/james/.nuget/packages/superfluid/0.0.1/analyzers/dotnet/cs/SuperFluid.dll : Exception has been thrown by the target of an invocation.. [/home/james/repos/SuperFluid/src/DemoProject/DemoProject.csproj]

The (abridged) csproj for my source generator is:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <LangVersion>default</LangVersion>
        <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
        <IncludeBuildOutput>false</IncludeBuildOutput>
        <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    </PropertyGroup>

    <ItemGroup>
        <None Include="../../README.md" Pack="true" PackagePath="\" />
    </ItemGroup>

    <ItemGroup>
        <InternalsVisibleTo Include="SuperFluid.Tests" />
    </ItemGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
        <PackageReference Include="YamlDotNet" Version="13.1.0" PrivateAssets="all" GeneratePathProperty="true" />
    </ItemGroup>
    
    <!-- Gross hack to let source generator use nuget packages -->
    <PropertyGroup>
        <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
    </PropertyGroup>
    
    <Target Name="GetDependencyTargetPaths" AfterTargets="ResolvePackageDependenciesForBuild">
            <ItemGroup>
                <TargetPathWithTargetPlatformMoniker Include="@(ResolvedCompileFileDefinitions)" IncludeRuntimeDependency="false" />
                <None Include="@(ResolvedCompileFileDefinitions)" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
            </ItemGroup>
    </Target>
    <!-- End Hack -->
    
    
    <ItemGroup>
        <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
    </ItemGroup>
</Project>

And my test project is:

<Project Sdk="Microsoft.NET.Sdk">

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

    <ItemGroup>
      <AdditionalFiles Include="DemoApiDefinition.fluid.yml" />
    </ItemGroup>

    <ItemGroup>
      <PackageReference Include="SuperFluid" Version="0.0.1" />
    </ItemGroup>

</Project>

Does anyone know what might be causing this? If not, how can I get the details of this inner exception?


Edit: I've retargetted my Source Generator to use netstandard2.0 and that doesn't seem to have helped:

I've also noticed this warning that may or may not be relevant when I pack:

/usr/share/dotnet/sdk/7.0.105/Sdks/NuGet.Build.Tasks.Pack/build/NuGet.Build.Tasks.Pack.targets(221,5): warning NU5128: - Add lib or ref assemblies for the netstandard2.0 target framework

brads3290
  • 1,926
  • 1
  • 14
  • 20
ScottishTapWater
  • 3,656
  • 4
  • 38
  • 81
  • Hi, source generators have to target netstandard2.0, not net7.0. – Hayden Jun 13 '23 at 22:12
  • @Hayden - I've tried retargetting and that's not solved the problem – ScottishTapWater Jun 18 '23 at 02:09
  • Can you explain a bit about that hack to let your source generator use nuget packages? As far as I know, you shouldn’t require a hack since the Roslyn compiler stuff is provided as a nuget package already - in other words, all source generators use at least those 2 nuget packages already so I’m confused why this hack is necessary. – brads3290 Jun 18 '23 at 08:45
  • @brads3290 - It's needed to use the YamlDotNet package (as well as any other nuget packages I want my analyser to use down the line) – ScottishTapWater Jun 18 '23 at 17:30
  • YamlDotNet appears to be available for netstandard2.0 so I guess I still don’t understand why the hack is necessary? Why can’t you just install it like normal? – brads3290 Jun 18 '23 at 18:45
  • Because the analyzer won't have access to it in another project unless you pack it up with the analyzer – ScottishTapWater Jun 18 '23 at 18:48
  • @brads3290 - I can't find the source I used originally for this, but this article sums it up https://www.meziantou.net/packaging-a-roslyn-analyzer-with-nuget-dependencies.htm That being said, I might try what's in there – ScottishTapWater Jun 18 '23 at 18:58
  • I've just tried the version in that article and get the same error – ScottishTapWater Jun 18 '23 at 19:22
  • @ScottishTapWater I'm trying to replicate your issue atm. Looks like I get the same result (even without packing) by throwing an exception in the constructor of the source generator. Could you please post the constructor (and any inline-initialised members) of your source generator? – brads3290 Jun 18 '23 at 19:28
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254132/discussion-between-brads3290-and-scottishtapwater). – brads3290 Jun 18 '23 at 19:30

1 Answers1

2

There are a few issues at play here which made debugging this issue quite complex. I'll break my answer into parts which hopefully provide a bit of signposting for future readers about how to go about debugging something like this.

Part 1 - The warning

You mentioned you got this warning:

CSC : warning CS8032: An instance of analyzer SuperFluid.Internal.SourceGenerators.FluidApiSourceGenerator cannot be created from /home/james/.nuget/packages/superfluid/0.0.1/analyzers/dotnet/cs/SuperFluid.dll : Exception has been thrown by the target of an invocation.. [/home/james/repos/SuperFluid/src/DemoProject/DemoProject.csproj]

It does seem a bit unhelpful at first, but there is some key information:

An instance of ... FluidApiSourceGenerator cannot be created

This means that the compiler is trying to create an instance of your generator and failing at that point. It's not even getting to the point of calling Initialize or running the source generator pipeline.

So you should be looking in your source generator's constructor, and any properties/fields that are initialised inline.

Part 2 - Getting more information from the TargetInvocationException

The second part of your question deals with the "Exception has been thrown by the target of an invocation" part of the warning.

This exception is being thrown when the compiler process tries to instantiate your class using reflection. Ideally we need to move the error-prone code to somewhere where we can log the exception somehow.

One option is to temporarily move the constructor logic into the Initialize method so that we have control over how the exception is handled, and can "log" it using ReportDiagnostic, so the real exception (not the TargetInvocationException) comes out as a compiler error during the build process.

(for future readers, the below code is based on the full generator source, from the repository provided in this extended discussion)

Something like this:

[Generator]
internal class FluidApiSourceGenerator : IIncrementalGenerator {
    
    // Remove temporarily
    // private readonly FluidGeneratorService _generatorService;
    
    // 1. Add a descriptor for when we need to output fatal errors
    private static readonly DiagnosticDescriptor FatalErrorOccurred = new(
        id: "E1",
        title: "Error occurred",
        messageFormat: "Error: '{0}'",
        category: "CodeGeneration",
        defaultSeverity: DiagnosticSeverity.Error,
        isEnabledByDefault: true,
        description: "Error occurred during code generation.");


    public FluidApiSourceGenerator() { }

    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        IncrementalValuesProvider<AdditionalText> extraTexts = context.AdditionalTextsProvider.Where(f => f.Path.EndsWith(".fluid.yml"));
        IncrementalValuesProvider<(string Name, string Content)> namesAndContents = extraTexts.Select((text, cancellationToken) => (Name: Path.GetFileNameWithoutExtension(text.Path), Content: text.GetText(cancellationToken)!.ToString()));
        context.RegisterSourceOutput(namesAndContents, (context, nameAndContent) => {
            try {
                // 3. Call the constructor logic to get an instance of FluidGeneratorService,
                // catching any exceptions
                var _generatorService = ConstructorLogic();

                Dictionary<string, string> generatedSource = _generatorService.Generate(nameAndContent.Content);
                foreach (KeyValuePair<string, string> kvp in generatedSource) {
                    context.AddSource(kvp.Key, kvp.Value);
                }
            } catch (Exception e) {
                // 4. Report exceptions as compiler errors.
                var diagnostic = Diagnostic.Create(FatalErrorOccurred, Location.None, e.ToString());
                context.ReportDiagnostic(diagnostic);
            }
        });
    }

    // 2. Move the logic that used to be in the constructor, to here
    private FluidGeneratorService ConstructorLogic() {
        IDeserializer deserializer = new DeserializerBuilder().WithNamingConvention(NullNamingConvention.Instance).Build();
        return new FluidGeneratorService(deserializer, new());
    }

}

Doing this, we see that the real exception is:

'System.IO.FileNotFoundException: Could not load file or assembly 'YamlDotNet, Version=13.0.0.0, Culture=neutral, PublicKeyToken=ec19458f3c15af5e'. The system cannot find the file specified.

Edit - Extracting the constructor logic into a separate method (rather than just doing it inside the try) is actually important. This is because of the nature of this specific issue (dependency not being found).

Dependencies used within a method are all linked the first time the method is called, but before any code inside is executed. This means that if we do the constructor logic "inline" within the try block, the exception will be thrown before we get into the try block, so we won't catch it.

See this answer.

Part 3 - Resolving the FileNotFoundException

To use 3rd party Nuget packages, Roslyn source generators must have those packages bundled as private dependencies.

I downloaded your package from Nuget, and YamlDotNet.dll isn't in there, which explains the FileNotFoundException: Image showing only SuperFluid.dll in the package, no dependencies

Now, when I cloned your repository and built your SuperFluid project, the generated package did include all the dependencies: Image showing all the relevant dependencies in the package

Based on that, I suspect there's something in your CD pipeline that's not playing nice. I don't have access to that configuration so I can't give you a definitive answer on that, but I can tell you what seems to be working for me:

  • Make sure you include GeneratePackageOnBuild in your build file:
<PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    
    <!-- other stuff -->

    <GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Generates a package at build -->
</PropertyGroup>
  • Make sure the PackageReference for your 3rd party dependency includes PrivateAssets="all" and GeneratePathProperty="true" (I don't think this is directly relevant to the issue at hand, but it is best practice to ensure that you're not introducing your generator dependencies as dependencies on the consuming project)
<PackageReference Include="YamlDotNet" Version="13.1.1" PrivateAssets="all" GeneratePathProperty="true" />
  • Make sure you're copying the dependencies to the analyzers/dotnet/cs folder (the "gross hack" from your question):
<PropertyGroup>
    <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>

<Target Name="GetDependencyTargetPaths">
    <ItemGroup>
        <TargetPathWithTargetPlatformMoniker Include="@(ResolvedCompileFileDefinitions)" IncludeRuntimeDependency="false" />
        <None Include="@(ResolvedCompileFileDefinitions)" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
    </ItemGroup>
</Target>

Once all that is done - which in your case, it is, run dotnet build (or build via your IDE) to build the generator (not the consuming project / demo project). This will create a .nupkg file:

bin/Release/SuperFluid.0.0.1-alpha.nupkg

Then do whatever you need to do with this file - publish to NuGet, install locally, whatever.

As for configuring your CD pipeline with this in mind, that's beyond the scope of discussion here, so I'll leave that to you.

Part 3.1 - The FileNotFoundException in the Demo Project

As a bonus - there was actually a separate issue causing the same result in the Demo Project, which had to do with how the ProjectReference was set up:

<ProjectReference Include="..\SuperFluid\SuperFluid.csproj"
                        PrivateAssets="all"
                        ReferenceOutputAssembly="false"
                        OutputItemType="Analyzer"
                        SetTargetFramework="TargetFramework=netstandard2.0"/>

Remove SetTargetFramework="TargetFramework=netstandard2.0" and it will work.

I'm not 100% sure why dependencies aren't copied correctly when this is present, but the SetTargetFramework property doesn't really apply here:

  • SetTargetFramework is used when referencing multi-targeted dependencies. So, if SuperFluid targeted both netstandard2.0 and net7.0, you would use SetTargetFramework to pick which one you wanted.
  • It being present seems to cause the issue you're seeing when SuperFluid only targets one framework (which, being a source generator, is all it's allowed to target).
  • Interestingly, adding a second target framework to SuperFluid (even though this is not supported), actually makes the problem go away as well - so dependencies apparently get copied correctly when SetTargetFramework is used correctly, but don't when it's not. If anyone knows why this happens, I'd love to know.
    • Like I said before though, source generators should target netstandard2.0, so the solution is for SuperFluid to target a single framework, and for SetTargetFramework to be removed.
brads3290
  • 1,926
  • 1
  • 14
  • 20
  • 1
    I've not had a chance to implement your suggestions yet, but your work has been so thorough I'm awarding the bounty, thank you! – ScottishTapWater Jun 21 '23 at 21:12
  • @ScottishTapWater Thank you! Do let me know how you go – brads3290 Jun 21 '23 at 23:38
  • I've managed to replicate your discovery by manually using the pack command and opened a separate question relating to it here: https://stackoverflow.com/questions/76551794/dotnet-build-and-dotnet-pack-produce-different-nupkgs – ScottishTapWater Jun 25 '23 at 18:49