dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
18.71k stars 3.98k forks source link

Source Generators referencing nuget packages cannot be debugged in VS #74027

Open raffaeler opened 2 weeks ago

raffaeler commented 2 weeks ago

Version Used: 4.10.0

Steps to Reproduce:

  1. Create a C# Source Generator
  2. Reference System.Text.Json and try to deserialize a string in the Execute method of a Source Generator An exception occurs when entering in any methods using the json classes:
    System.IO.FileNotFoundException: 'Could not load file or assembly 'System.Text.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' or one of its dependencies. The system cannot find the file specified.'

According to the source generators guide I should create a nuget package to get the System.Text.Json loaded in VS. Anyway, there is no mention on how to benefit from the <IsRoslynComponent>true</IsRoslynComponent> csproj property which allows to debug a generator referenced in the same solution of the generator.

The following official sample does not address the issue: https://github.com/dotnet/roslyn-sdk/blob/main/samples/CSharp/SourceGenerators/SourceGeneratorSamples/CSharpSourceGeneratorSamples.csproj

Repro code

using System;
using System.Linq;
using System.Text.Json;
using Microsoft.CodeAnalysis;

namespace MyCodegen;

[Generator]
public class TestGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var x = Deserialize();    // exception here when trying to load the referenced assembly
        context.AddSource("SiblingContextGenerator", $"// {x}");
    }

    private string? Deserialize()
        => JsonSerializer.Deserialize<string>("\"abc\"");
}

The csproj (with various tests) is the following:

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

    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <LangVersion>latest</LangVersion>
        <Nullable>enable</Nullable>
        <IsRoslynComponent>true</IsRoslynComponent>
        <NoWarn>NU5128</NoWarn>
        <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>

    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
        <PackageReference Include="System.Text.Json" Version="8.0.3"
                          PrivateAssets="all"
                          GeneratePathProperty="true" />
    </ItemGroup>

    <PropertyGroup>
        <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
    </PropertyGroup>

    <Target Name="GetDependencyTargetPaths">
        <ItemGroup>
            <!--<TargetPathWithTargetPlatformMoniker Include="$(PKGCsvTextFieldParser)\lib\netstandard2.0\CsvTextFieldParser.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGHandlebars_Net)\lib\netstandard2.0\Handlebars.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" />-->
            <TargetPathWithTargetPlatformMoniker Include="$(PkgSystem_Text_Json)\lib\netstandard2.0\System.Text.Json.dll" IncludeRuntimeDependency="false" />
        </ItemGroup>
    </Target>   

    <ItemGroup>
        <None Include="$(OutputPath)\$(AssemblyName).dll"
              Pack="true"
              PackagePath="analyzers/dotnet/cs"
              Visible="false" />

        <None Include="$(PkgSystem_Text_Json)\lib\netstandard2.0\*.dll"
              Pack="true"
              PackagePath="analyzers/dotnet/cs"
              Visible="false" />
    </ItemGroup>

    <PropertyGroup>
        <!-- Generates a package at build -->
        <GeneratePackageOnBuild>true</GeneratePackageOnBuild>

        <!-- Do not include the generator as a lib dependency -->
        <IncludeBuildOutput>false</IncludeBuildOutput>

        <PackageId>ABC</PackageId>
        <PackOnBuild>true</PackOnBuild>
        <DebugType>embedded</DebugType>
        <DebugSymbols>true</DebugSymbols>
    </PropertyGroup>

    <ItemGroup>
        <!-- Package the generator in the analyzer directory of the nuget package -->
        <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
    </ItemGroup>

</Project>

Expected Behavior: I should be able to reference external NuGet packages that are compliant with the operations allowed in the Source Code Generators.

Actual Behavior: Source generators referencing NuGet packages cannot be debugged with the provided Visual Studio (default) mechanism.

jaredpar commented 2 weeks ago

When you are debugging is System.Text.Json in the same directory as your generator? If not that is the source of the problem here.

raffaeler commented 1 week ago

Thank you @jaredpar for the answer. Two days ago I added the following settings in the csproj and it worked for the reasons you mentioned: <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

Is this the right way to debug a generator or there is a better way to do it? There is no clue about this issue in the generators cookbook document or in the roslyn-sdk repository where the samples are.

In other words: what is the intended setting that I should apply in order to debug a generator? What other documentation should I follow?

jaredpar commented 1 week ago

In other words: what is the intended setting that I should apply in order to debug a generator?

It is hard to give 100% "do this in your project file" guidance because the answer can vary a bit on how projects are setup. The best way to think about this is that you need to structure your build so that the bin directory has the same files as the NuPkg you use to deploy the generator.

raffaeler commented 1 week ago

Let's put in a different way then. What are the possible options to make this happen for a standalone generator that have both nuget and other projects references? I'd like to avoid batch files or msbuild steps with manual copies which are usually a mess to debug

Thank you

jaredpar commented 1 week ago

Do you have a sample project you're working with that we could look at?

Note: in general System.Text.Json is one of the harder dependencies. That is because it's so often a part of the process that is hosting the compiler. For example Visual Studio ships a version. That means generators are rarely in control of the actual version used at runtime.

raffaeler commented 1 week ago

The project has 5 generators + 1 dummy generator which apparently helps in forcing loading the correct version of System.Text.Json

The dummy generator is the test generator that I posted previously in this thread.

If I remove it, I get this exception while building:

Severity    Code    Description Project File    Line    Suppression State   Details
Error (active)  EFSC01  An exception was generated: 'The type initializer for 'EFCodegen.Configuration.AdHocJsonContext' threw an exception.'   CopyDataService ...\src\CopyDataService\CSC 1       

AdHoJsonContext is:

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(TargetGeneratorConfig[]))]
[JsonSerializable(typeof(List<TargetGeneratorConfig>))]
[JsonSerializable(typeof(TargetGeneratorConfig))]
internal partial class AdHocJsonContext : JsonSerializerContext
{
}

But removing the context does not help in getting a reliable compilation.

I would like to understand how to control the dependency, I can rely on older versions as well but I don´t which one I should reference. Could you please help me?

jaredpar commented 1 week ago

Could you please help me?

Hard to help without a more complete sample here.