0

i have made an extension du jsonNode by using IIncrementalGenerator. The purpose of the generator is search an Attribute named JsonAccessor, and passes the argmuent attribute to the underneath method. The implementation of method is generated at compilation. Everything works well but by unit tests fail because an exception is raised telling me that Nodes is not found in System.Text.Json.

Here is the code of my generator :

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

    }
}

and here is the unit test :

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

    }

The exception :

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

I precise that it'is only under tests that i have this problem. When I run the generator in nominal mode it works well.

Any idea about what the problem is ?

Thanks for the help !

I have tried to add in csproj of the test project.

Here is the code in 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>
2
  • 1
    I have solved the problem by injecting in the Test class the missing reference : internal static class 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"))); } .... Commented Sep 25, 2023 at 10:02
  • Glad to hear you resolved the issue. If you want, you might add a self-answer rather than a comment explaining how your solution. Commented Sep 26, 2023 at 16:35

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.