使用 IncrementalGenerator 时在 System.Text.Json 中找不到节点

Nodes not found in System.Text.Json when using IncrementalGenerator

提问人:Didier Jacquart 提问时间:9/21/2023 更新时间:9/21/2023 访问量:40

问:

我使用 IIncrementalGenerator 对 jsonNode 进行了扩展。生成器的用途是搜索名为 JsonAccessor 的 Attribute,并将 argmuent 属性传递给 underneath 方法。方法的实现在编译时生成。 一切正常,但单元测试失败,因为引发异常告诉我在 System.Text.Json 中找不到 Nodes。

这是我的生成器的代码:

using System.CodeDom.Compiler;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Package.JsonNodeExtension.Generator
{
    [Generator(LanguageNames.CSharp)]
    public sealed class GetValueFromPathGenerator : IIncrementalGenerator
    {
        private const string JsonPathAccessorAttribute = @"// <auto-generated/>

namespace Roslyn.Generated;

using System;

public sealed class JsonPathAccessorAttribute : Attribute
{    
    public string Path;

    public JsonPathAccessorAttribute(string path)
    {        
        Path = path;  
    }
}
";
        private readonly record struct InfoContext(string Namespace, Accessibility MethodAccessibility, Accessibility ClassAccessibility, string ClassName, string MethodName, string Path);

        private static readonly Regex IsArrayRegex = new(@"(?<propertyName>.*)\[(?<index>\d+)\]", RegexOptions.Compiled);

        public void Initialize(IncrementalGeneratorInitializationContext context)
        {
            //Build Attribute
            context.RegisterPostInitializationOutput(PostInitializationCallBack);

            var provider = context.SyntaxProvider.ForAttributeWithMetadataName("Roslyn.Generated.JsonPathAccessorAttribute",
                    static bool (node, cancellationToken) => 
                        node is MethodDeclarationSyntax method
                        && method.Modifiers.Any(SyntaxKind.StaticKeyword)
                        && method.Modifiers.Any(SyntaxKind.PartialKeyword),

                    (static InfoContext (context, cancellationToken) =>
                    {
                        Debug.Assert(context.TargetNode is MethodDeclarationSyntax);
                        Debug.Assert(context.TargetSymbol is IMethodSymbol);
                        Debug.Assert(!context.Attributes.IsEmpty);

                        var symbol = Unsafe.As<IMethodSymbol>(context.TargetSymbol);

                        var methodDeclarationSyntax = Unsafe.As<MethodDeclarationSyntax>(context.TargetNode);
                        var path = GetPathFromAttribute(methodDeclarationSyntax);

                        var baseDeclarationSyntax = Unsafe.As<BaseTypeDeclarationSyntax>(context.TargetNode);
                        var infoContext = new InfoContext
                        {
                            Namespace = GetNamespace(baseDeclarationSyntax),
                            ClassName = symbol.ContainingType.Name,
                            ClassAccessibility = symbol.ContainingType.DeclaredAccessibility,
                            MethodAccessibility = symbol.DeclaredAccessibility,
                            MethodName = symbol.Name,
                            Path = path!
                        };

                        return infoContext;
                    })!);


            //Add source code
            context.RegisterSourceOutput(provider, Execute);
        }

        private static string? GetPathFromAttribute(MethodDeclarationSyntax methodDeclarationSyntax)
        {
            string? path = null;
            foreach (var attribute in methodDeclarationSyntax.AttributeLists.SelectMany(attributeList => attributeList.Attributes))
            {
                if (attribute.ArgumentList is not { Arguments.Count : >= 1 } argumentList ||
                    attribute.Name.ToString() != "JsonPathAccessor")
                {
                    continue;
                }

                var argument = argumentList.Arguments[0];

                return argument.Expression.ToString();
            }

            return path;
        }

        private static void Execute(SourceProductionContext context, InfoContext info)
        {
            using StringWriter writer = new(CultureInfo.InvariantCulture);
            using IndentedTextWriter source = new(writer);
            source.WriteLine("// <auto-generated/>");
            source.WriteLine("");
            source.WriteLine("using System;");
            source.WriteLine("using System.Text;");
            source.WriteLine("using System.Text.Json;");
            source.WriteLine("using System.Text.Json.Nodes;");
            source.WriteLine("using System.ComponentModel;");
            source.WriteLine("using System.Globalization;");

            source.WriteLine("");
            source.WriteLine($"namespace {info.Namespace};");
            source.WriteLine("");
            source.WriteLine("#nullable enable");
            source.WriteLine($"{info.ClassAccessibility.ToString().ToLower()} static partial class {info.ClassName}");
            source.WriteLine("{");
            source.Indent++;
            source.WriteLine($"{info.MethodAccessibility.ToString().ToLower()} static partial T? {info.MethodName}<T>(this JsonNode? jsonNode, bool? convertNumberMode, JsonSerializerOptions? options)");
            source.WriteLine("{");
            source.Indent++;
            source.WriteLine($"var node = jsonNode?{ConvertsStringPathToJsonNodePath(info.Path)};");
            source.WriteLine("if(node == null)");
            source.WriteLine("{");
            source.Indent++;
            source.WriteLine("return default(T);");
            source.Indent--;
            source.WriteLine("}");
            source.WriteLine("");
            source.WriteLine("if(node is not JsonValue)");
            source.WriteLine("{");
            source.Indent++;
            source.WriteLine("return node.Deserialize<T>(options);");
            source.Indent--;
            source.WriteLine("}");
            source.WriteLine("");

            source.WriteLine("if(convertNumberMode == true)");
            source.WriteLine("{");
            source.Indent++;
            source.WriteLine("return ConvertToExpectedType<T>(node);");
            source.Indent--;
            source.WriteLine("}");
            source.WriteLine("");

            source.WriteLine("return node.GetValue<T>();");

            source.Indent--;
            source.WriteLine("}");
            source.WriteLine("");
            source.WriteLine("private static T? ConvertToExpectedType<T>(JsonNode? node)");
            source.WriteLine("{");
            source.Indent++;
            source.WriteLine("if (string.IsNullOrEmpty(node?.ToJsonString()))");
            source.WriteLine("{");
            source.Indent++;
            source.WriteLine("return default;");
            source.Indent--;
            source.WriteLine("}");
            source.WriteLine("");
            source.WriteLine("var converter = TypeDescriptor.GetConverter(typeof(T));");
            source.WriteLine("var utf8JsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes(node.ToString()!));");
            source.WriteLine("utf8JsonReader.Read();");
            source.WriteLine("if (utf8JsonReader.TryGetInt32(out var intValue))");
            source.WriteLine("{");
            source.Indent++;
            source.WriteLine("return (T)converter.ConvertFrom(intValue.ToString(CultureInfo.InvariantCulture))!;");
            source.Indent--;
            source.WriteLine("}");
            source.WriteLine("if (utf8JsonReader.TryGetDouble(out var doubleValue))");
            source.WriteLine("{");
            source.Indent++;
            source.WriteLine("return (T)converter.ConvertFrom(doubleValue.ToString(CultureInfo.InvariantCulture))!;");
            source.Indent--;
            source.WriteLine("}");
            source.WriteLine("return default(T);");
            source.Indent--;
            source.WriteLine("}");


            source.Indent--;
            source.WriteLine("}");
            source.WriteLine("#nullable disable");

            Debug.Assert(source.Indent == 0);
            context.AddSource($"{info.Namespace}.{info.ClassName}.{info.MethodName}.g.cs", writer.ToString());
        }

        private static string GetNamespace(BaseTypeDeclarationSyntax syntax)
        {
            // If we don't have a namespace at all we'll return an empty string
            // This accounts for the "default namespace" case
            string nameSpace = string.Empty;

            // Get the containing syntax node for the type declaration
            // (could be a nested type, for example)
            SyntaxNode? potentialNamespaceParent = syntax.Parent;

            // Keep moving "out" of nested classes etc until we get to a namespace
            // or until we run out of parents
            while (potentialNamespaceParent != null &&
                   potentialNamespaceParent is not NamespaceDeclarationSyntax
                   && potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax)
            {
                potentialNamespaceParent = potentialNamespaceParent.Parent;
            }

            // Build up the final namespace by looping until we no longer have a namespace declaration
            if (potentialNamespaceParent is BaseNamespaceDeclarationSyntax namespaceParent)
            {
                // We have a namespace. Use that as the type
                nameSpace = namespaceParent.Name.ToString();

                // Keep moving "out" of the namespace declarations until we 
                // run out of nested namespace declarations
                while (true)
                {
                    if (namespaceParent.Parent is not NamespaceDeclarationSyntax parent)
                    {
                        break;
                    }

                    // Add the outer namespace as a prefix to the final namespace
                    nameSpace = $"{namespaceParent.Name}.{nameSpace}";
                    namespaceParent = parent;
                }
            }

            // return the final namespace
            return nameSpace;
        }



        private static string ConvertsStringPathToJsonNodePath(string path)
        {
            if (string.IsNullOrEmpty(path))
            {
                throw new ArgumentException("path should not be null", nameof(path));
            }

            //Remove first and last "
            path = path.Replace("\"", "");

            var pathArrays = path.Split('.');
            var sb = new StringBuilder();

            var lastElementName = pathArrays.LastOrDefault();

            foreach (var propertyName in pathArrays)
            {
                var isLastElement = lastElementName == propertyName;

                var arrayMatch = IsArrayRegex.Match(propertyName);
                if (int.TryParse(arrayMatch.Groups["index"].Value, out var index))
                {
                    var propertyNameValue = arrayMatch.Groups["propertyName"].Value;
                    sb.Append(AddBracketsForArrays(propertyNameValue, index, isLastElement));
                }
                else
                {
                    sb.Append(AddBrackets(propertyName, isLastElement));
                }
            }
            return sb.ToString();
        }
        private static string AddBracketsForArrays(string propertyNameValue, int index, bool isLastElement)
        {
            var sb = new StringBuilder();
            sb.Append("[\"");
            sb.Append(propertyNameValue);
            sb.Append("\"]?");
            sb.Append("[");
            sb.Append(index);
            sb.Append("]");
            if (!isLastElement)
            {
                sb.Append("?");
            }
            return sb.ToString();
        }

        private static string AddBrackets(object property, bool isLastElement)
        {
            var sb = new StringBuilder();
            sb.Append("[\"");
            sb.Append(property);
            sb.Append("\"]");
            if (!isLastElement)
            {
                sb.Append("?");
            }
            return sb.ToString();
        }

        private static void PostInitializationCallBack(IncrementalGeneratorPostInitializationContext context)
        {
            context.AddSource("Roslyn.Generated.JsonPathAccessorAttribute.g.cs", JsonPathAccessorAttribute);
        }

    }
}

这是单元测试:

using System.CodeDom.Compiler;
using System.Globalization;
using Microsoft.CodeAnalysis.Testing;
using Xunit;
using VerifyCS = Package.JsonNodeExtension.Generator.UnitTests.Verifiers.CSharpSourceGeneratorVerifier<Package.JsonNodeExtension.Generator.GetValueFromPathGenerator>;

namespace Package.JsonNodeExtension.Generator.UnitTests;

public class GetValueFromPathGeneratorUnitTests
{
    private const string Attribute = @"// <auto-generated/>

namespace Roslyn.Generated;

using System;

public sealed class JsonPathAccessorAttribute : Attribute
{    
    public string Path;

    public JsonPathAccessorAttribute(string path)
    {        
        Path = path;  
    }
}
";

    

    [Fact]
    public async Task Generator_WithCandidates_AddPartialMethods()
    {
        const string code = @"
using System;
using System.Text.Json;
using System.Text.Json.Nodes;
using Roslyn.Generated;

namespace MyNamespace.Tests;

#nullable enable
public static partial class PartialClass
{
    [JsonPathAccessor(""testPath"")]
    public static partial T? GetField<T>(this JsonNode? jsonNode, bool? convertNumberMode, JsonSerializerOptions? options);
}
#nullable disable";

        const string generated = @"// <auto-generated/>

using System;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.ComponentModel;
using System.Globalization;

namespace MyNamespace.Tests;

#nullable enable
public static partial class PartialClass
{
    public static partial T? GetField<T>(this JsonNode? jsonNode, bool? convertNumberMode, JsonSerializerOptions? options)
    {
        var node = jsonNode?[""testPath""];
        if(node == null)
        {
            return default(T);
        }
        
        if(node is not JsonValue)
        {
            return node.Deserialize<T>(options);
        }
        
        if(convertNumberMode == true)
        {
            return ConvertToExpectedType<T>(node);
        }
        
        return node.GetValue<T>();
    }
    
    private static T? ConvertToExpectedType<T>(JsonNode? node)
    {
        if (string.IsNullOrEmpty(node?.ToJsonString()))
        {
            return default;
        }
        
        var converter = TypeDescriptor.GetConverter(typeof(T));
        var utf8JsonReader = new Utf8JsonReader(Encoding.UTF8.GetBytes(node.ToString()!));
        utf8JsonReader.Read();
        if (utf8JsonReader.TryGetInt32(out var intValue))
        {
            return (T)converter.ConvertFrom(intValue.ToString(CultureInfo.InvariantCulture))!;
        }
        if (utf8JsonReader.TryGetDouble(out var doubleValue))
        {
            return (T)converter.ConvertFrom(doubleValue.ToString(CultureInfo.InvariantCulture))!;
        }
        return default(T);
    }
}
#nullable disable
";

//here an exception is raised 
        await VerifyCS.VerifyGeneratorAsync(code, ("Roslyn.Generated.JsonPathAccessorAttribute.g.cs", Attribute), ("MyNamespace.Tests.PartialClass.GetField.g.cs", generated));

    }

例外情况:

Message: 
Microsoft.CodeAnalysis.Testing.Verifiers.EqualWithMessageException : Context: Diagnostics of test state
Mismatch between number of diagnostics returned, expected "0" actual "7"

Diagnostics:
// /0/Test0.cs(4,24): error CS0234: Le nom de type ou d'espace de noms 'Nodes' n'existe pas dans l'espace de noms 'System.Text.Json' (vous manque-t-il une référence d'assembly ?)
DiagnosticResult.CompilerError("CS0234").WithSpan(4, 24, 4, 29).WithArguments("Nodes", "System.Text.Json"),
// /0/Test0.cs(13,47): error CS0246: Le nom de type ou d'espace de noms 'JsonNode' est introuvable (vous manque-t-il une directive using ou une référence d'assembly ?)
DiagnosticResult.CompilerError("CS0246").WithSpan(13, 47, 13, 55).WithArguments("JsonNode"),
// Package.JsonNodeExtension.Generator\Package.JsonNodeExtension.Generator.GetValueFromPathGenerator\MyNamespace.Tests.PartialClass.GetField.g.cs(7,24): error CS0234: Le nom de type ou d'espace de noms 'Nodes' n'existe pas dans l'espace de noms 'System.Text.Json' (vous manque-t-il une référence d'assembly ?)
DiagnosticResult.CompilerError("CS0234").WithSpan("Package.JsonNodeExtension.Generator\Package.JsonNodeExtension.Generator.GetValueFromPathGenerator\MyNamespace.Tests.PartialClass.GetField.g.cs", 7, 24, 7, 29).WithArguments("Nodes", "System.Text.Json"),
// Package.JsonNodeExtension.Generator\Package.JsonNodeExtension.Generator.GetValueFromPathGenerator\MyNamespace.Tests.PartialClass.GetField.g.cs(16,47): error CS0246: Le nom de type ou d'espace de noms 'JsonNode' est introuvable (vous manque-t-il une directive using ou une référence d'assembly ?)
DiagnosticResult.CompilerError("CS0246").WithSpan("Package.JsonNodeExtension.Generator\Package.JsonNodeExtension.Generator.GetValueFromPathGenerator\MyNamespace.Tests.PartialClass.GetField.g.cs", 16, 47, 16, 55).WithArguments("JsonNode"),
// Package.JsonNodeExtension.Generator\Package.JsonNodeExtension.Generator.GetValueFromPathGenerator\MyNamespace.Tests.PartialClass.GetField.g.cs(24,24): error CS0103: Le nom 'JsonValue' n'existe pas dans le contexte actuel
DiagnosticResult.CompilerError("CS0103").WithSpan("Package.JsonNodeExtension.Generator\Package.JsonNodeExtension.Generator.GetValueFromPathGenerator\MyNamespace.Tests.PartialClass.GetField.g.cs", 24, 24, 24, 33).WithArguments("JsonValue"),
// Package.JsonNodeExtension.Generator\Package.JsonNodeExtension.Generator.GetValueFromPathGenerator\MyNamespace.Tests.PartialClass.GetField.g.cs(37,48): error CS0246: Le nom de type ou d'espace de noms 'JsonNode' est introuvable (vous manque-t-il une directive using ou une référence d'assembly ?)
DiagnosticResult.CompilerError("CS0246").WithSpan("Package.JsonNodeExtension.Generator\Package.JsonNodeExtension.Generator.GetValueFromPathGenerator\MyNamespace.Tests.PartialClass.GetField.g.cs", 37, 48, 37, 56).WithArguments("JsonNode"),
// Package.JsonNodeExtension.Generator\Package.JsonNodeExtension.Generator.GetValueFromPathGenerator\MyNamespace.Tests.PartialClass.GetField.g.cs(45,72): error CS8604: Existence possible d'un argument de référence null pour le paramètre 's' dans 'byte[] Encoding.GetBytes(string s)'.
DiagnosticResult.CompilerError("CS8604").WithSpan("Package.JsonNodeExtension.Generator\Package.JsonNodeExtension.Generator.GetValueFromPathGenerator\MyNamespace.Tests.PartialClass.GetField.g.cs", 45, 72, 45, 87).WithArguments("s", "byte[] Encoding.GetBytes(string s)"),


Assert.Equal() Failure
Expected: 0
Actual:   7

我精确地说,只有在测试下我才有这个问题。当我在标称模式下运行发电机时,它运行良好。

知道问题是什么吗?

感谢您的帮助!

我试图添加 在测试项目的 CSPROJ 中。

这是 csproj 中的代码:

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

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

    <PropertyGroup>
        <IsPackable>false</IsPackable>
        <NoWarn>$(NoWarn);CA1707</NoWarn>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.1" />
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.6.0" />
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
        <PackageReference Include="System.Text.Json" Version="7.0.3" />
        <PackageReference Include="xunit" Version="2.4.2" />
        <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" />
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\Package.JsonNodeExtension.Generator\Package.JsonNodeExtension.Generator.csproj" />
    </ItemGroup>

    <ItemGroup>
        <None Include="App.config" />
    </ItemGroup>

</Project>
量生成器 system.text.json 单元测试 命名空间

评论

1赞 Didier Jacquart 9/25/2023
我通过在 Test 类中注入缺少的引用解决了这个问题: 内部静态类 ReferenceAssembliesHelper { internal static readonly Lazy<ReferenceAssemblies> Default = new(() => new ReferenceAssemblies( targetFramework: “net6.0”, referenceAssemblyPackage: new PackageIdentity(“Microsoft.NETCore.App.Ref”, “6.0.0”), referenceAssemblyPath: Path.Combine(“ref”, “net6.0”))); } ....
0赞 dbc 9/27/2023
很高兴听到您解决了这个问题。如果需要,可以添加自我回答,而不是注释来解释您的解决方案。

答: 暂无答案