提问人:Didier Jacquart 提问时间:9/21/2023 更新时间:9/21/2023 访问量:40
使用 IncrementalGenerator 时在 System.Text.Json 中找不到节点
Nodes not found in System.Text.Json when using IncrementalGenerator
问:
我使用 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>
答: 暂无答案
评论