本文属于 IIncrementalGenerator 增量 Source Generator 源代码生成入门系列博客,本文将和大家介绍如何为源代码生成项目添加的单元测试里面提供 AnalyzerConfigOptionsProvider 选项

本文是 为 IIncrementalGenerator 增量 Source Generator 源代码生成项目添加单元测试 的后续。在上文介绍了如何给增量 Source Generator 源代码生成项目添加单元测试,本文将在此基础上,告诉大家如何提供 AnalyzerConfigOptionsProvider 选项

先来看看一个简单的源代码生成器的例子,以下的代码将根据配置的 FooProperty 属性决定生成的代码内容

[Generator(LanguageNames.CSharp)]
public class IncrementalGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        IncrementalValueProvider<string> configurationProvider = context.AnalyzerConfigOptionsProvider.Select((t, _) =>
        {
            var globalOptions = t.GlobalOptions;
            if (globalOptions.TryGetValue("build_property.FooProperty", out var property))
            {
                return property;
            }

            return null;
        });

        context.RegisterSourceOutput(configurationProvider, (productionContext, configurationProperty) =>
        {
            productionContext.AddSource("GeneratedCode.cs",
                $$"""
                  using System;
                  
                  namespace LurlelnarkallChijurjeaqelba
                  {
                      public static class GeneratedCode
                      {
                          public static void Print()
                          {
                              Console.WriteLine("配置的属性 {{configurationProperty}}");
                          }
                      }
                  }
                  """);
        });
    }
}

注: 为了让我的博客引擎开森,以上代码部分花括号被我替换为了全角花括号。大家在使用的时候需要将全角花括号替换为半角花括号

以上的 build_property.FooProperty 就是获取某个属性配置的写法,正常来说是需要依靠 CompilerVisibleProperty 指定具体的属性名才能被源代码生成器访问到的,详细请参阅 IIncrementalGenerator 增量 Source Generator 生成代码入门 读取 csproj 项目文件的属性配置

在正式项目里面大概的写法如下:

  <PropertyGroup>
    <FooProperty>lindexi is doubi</FooProperty>
  </PropertyGroup>

  <ItemGroup>
    <CompilerVisibleProperty Include="FooProperty" />
  </ItemGroup>

以上代码既可以写到 csproj 项目文件里面,也可以被放在带在 NuGet 包里的 props 文件里面。如果对此机制感觉到陌生,还请参阅 dotnet 源代码生成器分析器入门

单元测试里面,需要使用 CSharpGeneratorDriver.Create 的重载方法传入 AnalyzerConfigOptionsProvider 类型参数

由于 AnalyzerConfigOptionsProvider 是抽象的,我添加了如下代码用于辅助测试

internal class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider
{
    public TestAnalyzerConfigOptionsProvider(Dictionary<string, string> configOptions)
    {
        var testAnalyzerConfigOptions = new TestAnalyzerConfigOptions(configOptions);
        GlobalOptions = testAnalyzerConfigOptions;
    }

    public override AnalyzerConfigOptions GetOptions(SyntaxTree tree)
    {
        return GlobalOptions;
    }

    public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
    {
        return GlobalOptions;
    }

    public override AnalyzerConfigOptions GlobalOptions { get; }
}

internal class TestAnalyzerConfigOptions : AnalyzerConfigOptions
{
    public TestAnalyzerConfigOptions(Dictionary<string, string> configOptions)
    {
        _configOptions = configOptions;
    }

    private readonly Dictionary<string, string> _configOptions;

    public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
    {
        return _configOptions.TryGetValue(key, out value);
    }
}

通过 TestAnalyzerConfigOptionsProvider 辅助代码,可以使用字典表示将要注入到测试里面的属性,代码如下

        // 创建 AnalyzerConfigOptions
        var configOptions = new Dictionary<string, string>
        {
            ["build_property.FooProperty"] = "Test",
        };
        var analyzerConfigOptionsProvider = new TestAnalyzerConfigOptionsProvider(configOptions);

configOptions 传入到 CSharpGeneratorDriver.Create 方法里面,代码如下

        var compilation = CreateCompilation(...);
        var generator = new IncrementalGenerator();

        GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()], optionsProvider: analyzerConfigOptionsProvider);

这里需要让 IncrementalGenerator 通过 AsSourceGenerator 扩展方法转换为 ISourceGenerator 类型,才能传入此重载方法里面

完成以上步骤之后,就可以调用 GeneratorDriver.RunGenerators 开始执行源代码生成器

        driver = driver.RunGenerators(compilation);

以下是示例的单元测试


[TestClass]
public class IncrementalGeneratorTest
{
    [TestMethod]
    public void Test()
    {
        var testCode =
            """
            using System;
            
            namespace LurlelnarkallChijurjeaqelba;
            """;

        var compilation = CreateCompilation(testCode);
        var generator = new IncrementalGenerator();

        // 创建 AnalyzerConfigOptions
        var configOptions = new Dictionary<string, string>
        {
            ["build_property.FooProperty"] = "Test",
        };
        var analyzerConfigOptionsProvider = new TestAnalyzerConfigOptionsProvider(configOptions);

        GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()], optionsProvider: analyzerConfigOptionsProvider);
        driver = driver.RunGenerators(compilation);

        var runResult = driver.GetRunResult();
        Assert.HasCount(1, runResult.GeneratedTrees);
        foreach (var generatedTree in runResult.GeneratedTrees)
        {
            var generatedCode = generatedTree.ToString();
            Debug.WriteLine(generatedCode);

            if (generatedTree.FilePath.EndsWith("GeneratedCode.cs"))
            {
                var expected =
                    """
                    using System;
                    
                    namespace LurlelnarkallChijurjeaqelba
                    {
                        public static class GeneratedCode
                        {
                            public static void Print()
                            {
                                Console.WriteLine("配置的属性 Test");
                            }
                        }
                    }
                    """;

                // 防止拉取 git 时出现的 \r\n 不匹配问题。能够解决一些拉取 git 的奇怪的坑,也就是在我电脑上跑的好好的,但为什么在你电脑上就炸了
                expected = expected.Replace("\r\n", "\n");
                Assert.AreEqual(expected, generatedCode.Replace("\r\n", "\n"));
            }
        }
    }

    private static CSharpCompilation CreateCompilation(string source)
        => CSharpCompilation.Create("compilation",
            new[] { CSharpSyntaxTree.ParseText(source, path: "Foo.cs") },
            new MetadataReference[]
            {
                // 如果缺少引用,那将会导致单元测试有些符号无法寻找正确,从而导致解析失败
                // 在这里添加你自己的依赖库的引用
            }
            // 加上整个 dotnet 的基础库
            .Concat(MetadataReferenceProvider.GetDotNetMetadataReferenceList()),
            new CSharpCompilationOptions(OutputKind.ConsoleApplication));
}

internal class TestAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider
{
    public TestAnalyzerConfigOptionsProvider(Dictionary<string, string> configOptions)
    {
        var testAnalyzerConfigOptions = new TestAnalyzerConfigOptions(configOptions);
        GlobalOptions = testAnalyzerConfigOptions;
    }

    public override AnalyzerConfigOptions GetOptions(SyntaxTree tree)
    {
        return GlobalOptions;
    }

    public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
    {
        return GlobalOptions;
    }

    public override AnalyzerConfigOptions GlobalOptions { get; }
}

internal class TestAnalyzerConfigOptions : AnalyzerConfigOptions
{
    public TestAnalyzerConfigOptions(Dictionary<string, string> configOptions)
    {
        _configOptions = configOptions;
    }

    private readonly Dictionary<string, string> _configOptions;

    public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
    {
        return _configOptions.TryGetValue(key, out value);
    }
}

internal static class MetadataReferenceProvider
{
    public static IReadOnlyList<MetadataReference> GetDotNetMetadataReferenceList()
    {
        if (_cacheList is not null)
        {
            return _cacheList;
        }

        var metadataReferenceList = new List<MetadataReference>();
        var assembly = Assembly.Load("System.Runtime");
        foreach (var file in Directory.GetFiles(Path.GetDirectoryName(assembly.Location)!, "*.dll"))
        {
            try
            {
                metadataReferenceList.Add(MetadataReference.CreateFromFile(file));
            }
            catch
            {
                // 忽略
            }
        }

        _cacheList = metadataReferenceList;
        return _cacheList;
    }

    private static IReadOnlyList<MetadataReference>? _cacheList;
}

本文的代码放在 githubgitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 28cf0fb972be64983e4e5eb91d9dd62930f2d49d

以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 28cf0fb972be64983e4e5eb91d9dd62930f2d49d

获取代码之后,进入 Roslyn/LurlelnarkallChijurjeaqelba 文件夹,即可获取到源代码


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/IIncrementalGenerator-%E5%A6%82%E4%BD%95%E5%9C%A8%E6%BA%90%E4%BB%A3%E7%A0%81%E7%94%9F%E6%88%90%E5%99%A8%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E6%8F%90%E4%BE%9B-AnalyzerConfigOptionsProvider-%E9%80%89%E9%A1%B9.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

微软最具价值专家


无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区

以上广告全是友情推广,无盈利