C# 源代码生成器不包括来自项目参考的结果

Posted

技术标签:

【中文标题】C# 源代码生成器不包括来自项目参考的结果【英文标题】:C# Source Generator not including results from Project Reference 【发布时间】:2021-12-14 06:02:31 【问题描述】:

Edit3:在某些时候,这才刚刚开始工作。不知道为什么。也许是修复了一个 VS 错误?

Edit2:查看解决方案资源管理器中的 Analyzers 节点,我发现源生成器在我第一次打开程序时成功运行,然后它停止并且它生成的所有内容都消失了对我的代码进行了一些更改。

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.

编辑: 按照 cmets 的建议调用 Debugger.Launch() 后,我可以确认生成器代码正在运行,并且源文本看起来与预期的完全一样。但是 IDE 和编译器仍然会报错,就好像没有包含结果一样。

我正在尝试设置一个源代码生成器以从本地项目引用运行,但无法使其实际运行。我的 NUnit 测试通过了,所以我知道实际的生成逻辑很好,但是准系统测试项目在 Visual Studio 中都无法编译并报告错误。我正在使用 Visual Studio 2022 Preview 5.0,以防万一。

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

【问题讨论】:

【参考方案1】:

源生成器目标netstandard2.0,您的项目目标net6.0。 当您通过 PackageReference 使用源生成器时,这不是问题。

认为要让ProjectReference 在这种情况下工作,您需要添加SetTargetFramework 元数据。

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

这可能有效,抱歉现在不能尝试。

【讨论】:

这似乎不起作用 @Salvador 嗯,这太糟糕了。您可以尝试在您的 Initialize 和/或 Execute 方法中添加 System.Diagnostics.Debugger.Launch() 以查看它是否实际被调用。 @Salvador 另外,您没有将 [Generator] 属性放在生成器类上。我认为这也是需要的(ISourceGenerator 接口也是如此)。所以也许它真的没有运行 - 单元测试的工作方式不同,这可能并不重要。 不错,但恐怕也没有解决问题。 好吧,正如我在编辑中提到的,调试器确认我的代码正在运行,编译器/IDE 只是由于某种原因没有得到结果。【参考方案2】:

不幸的是,即使在当前的 VS 2022(版本 17.0.5)中,对该功能的支持也受到了一些限制。正如您所注意到的,VS 显示生成的代码的正确状态的唯一时刻是在 VS 重新启动之后(不仅仅是解决方案加载/卸载,而是应用程序的完全重新启动)。生成器完成后没问题,你只想检查生成了什么,但是在生成器开发过程中很痛苦。 所以我在开发过程中最终采用了这样的方法:

在给定生成器的调试/开发过程中,我们不仅可以将生成的文件的输出添加到编译上下文中,还可以添加到文件系统的临时目录中,或者仅添加到临时目录中,直到我们对结果满意为止。

要强制生成器运行,我们需要强制重建“testproject.csproj”项目。 我会使用“testproject”项目目录中的命令行:'dotnet clean; dotnet build'。

生成的文件将在输出目录中结束。例如,我们可以使用 VS Code 观看它们。 VS Code 不会阻止打开的文件,但任何其他具有非阻塞读取的记事本就足够了。 这不是一个理想的解决方案,但此时它消除了生成器开发的主要痛苦:看到代码生成的实际结果,我们不必重新启动 VS。

“generator.csproj”项目的示例代码草案:

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;
            while (!done)
            
                int cnt = 0;
                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);
                
            
        
    


【讨论】:

以上是关于C# 源代码生成器不包括来自项目参考的结果的主要内容,如果未能解决你的问题,请参考以下文章

是否可以仅使用 C# 以编程方式生成 X509 证书?

从 C# 调用 F# 代码

项目模板和 C# 生成中的 T4

部分代码未使用来自 RIA 服务的(重新)生成的代码进行编译

如何为 C# 项目生成 .tlh 文件

如何将C#代码转换成软件