mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-05 02:27:21 +00:00
Refused JSON contracts built by reflection and checked all of them in the trimmed build (#1152)
This commit is contained in:
1 parent
f864359290
commit
0b42c0288e
23 files changed
+535
-99
No files matched your search
@@ -0,0 +1,106 @@
|
||||
using AIStudio.Tests.Models.Generation;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
using SourceCodeRules.UsageAnalyzers;
|
||||
|
||||
namespace AIStudio.Tests.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that every way into JSON contracts built by reflection is refused while compiling.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The pre-release v26.10.1 failed because a published build lacked what reflection needed for the JSON of
|
||||
/// AI Studio. Since then, the options of AI Studio name a JsonSerializerContext, whose contracts are generated
|
||||
/// while compiling. A single call without options brings reflection back, and only a published build shows
|
||||
/// it, so MWAIS0015 turns such a call into a build error. These tests say which code it refuses and which it
|
||||
/// leaves alone.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class JsonSourceGenerationAnalyzerTests
|
||||
{
|
||||
[TestCase("JsonSerializer.Serialize(meeting)", "JsonSerializer.Serialize gets no options", TestName = "Writing without options")]
|
||||
[TestCase("JsonSerializer.Serialize(meeting, options: null)", "JsonSerializer.Serialize gets no options", TestName = "Writing with options which are null")]
|
||||
[TestCase("JsonSerializer.Deserialize<Meeting>(json)", "JsonSerializer.Deserialize gets no options", TestName = "Reading without options")]
|
||||
[TestCase("element.Deserialize<Meeting>()", "JsonSerializer.Deserialize gets no options", TestName = "Reading a JSON element without options")]
|
||||
[TestCase("client.GetFromJsonAsync<Meeting>(\"/meeting\")", "HttpClientJsonExtensions.GetFromJsonAsync gets no options", TestName = "Reading a web answer without options")]
|
||||
[TestCase("client.PostAsJsonAsync(\"/meeting\", meeting)", "HttpClientJsonExtensions.PostAsJsonAsync gets no options", TestName = "Sending a web request without options")]
|
||||
[TestCase("content.ReadFromJsonAsync<Meeting>()", "HttpContentJsonExtensions.ReadFromJsonAsync gets no options", TestName = "Reading web content without options")]
|
||||
[TestCase("JsonContent.Create(meeting)", "JsonContent.Create gets no options", TestName = "Creating web content without options")]
|
||||
[TestCase("JsonSerializer.Serialize(meeting, JsonSerializerOptions.Default)", "JsonSerializerOptions.Default builds every contract by reflection", TestName = "The default options")]
|
||||
[TestCase("JsonSerializerOptions.Web", "JsonSerializerOptions.Web builds every contract by reflection", TestName = "The options for the web")]
|
||||
[TestCase("new DefaultJsonTypeInfoResolver()", "DefaultJsonTypeInfoResolver builds contracts by reflection", TestName = "The resolver which uses reflection")]
|
||||
[TestCase("new JsonSerializerOptions { TypeInfoResolver = new DefaultJsonTypeInfoResolver() }", "DefaultJsonTypeInfoResolver builds contracts by reflection", TestName = "Options with the resolver which uses reflection")]
|
||||
[TestCase("new JsonSerializerOptions { WriteIndented = true }", "These options name no TypeInfoResolver", TestName = "Options without a resolver")]
|
||||
[TestCase("new JsonSerializerOptions(JsonSerializerDefaults.Web)", "These options name no TypeInfoResolver", TestName = "Options for the web without a resolver")]
|
||||
[TestCase("new JsonSerializerOptions { TypeInfoResolver = null }", "These options name no TypeInfoResolver", TestName = "Options with a resolver which is null")]
|
||||
public async Task AWayIntoReflectionIsRefused(string expression, string expectedMessage)
|
||||
{
|
||||
var reported = await AnalyzeAsync(expression);
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(reported, Has.Count.EqualTo(1));
|
||||
Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0015"));
|
||||
Assert.That(reported.FirstOrDefault()?.GetMessage(), Does.StartWith(expectedMessage));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OptionsCreatedByATargetTypedNewWithoutAResolverAreRefused()
|
||||
{
|
||||
var reported = await AnalyzeAsync("OTHER_OPTIONS", "private static readonly JsonSerializerOptions OTHER_OPTIONS = new() { WriteIndented = true };");
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(reported, Has.Count.EqualTo(1));
|
||||
Assert.That(reported.FirstOrDefault()?.GetMessage(), Does.StartWith("These options name no TypeInfoResolver"));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("JsonSerializer.Serialize(meeting, OPTIONS)", TestName = "Writing with options")]
|
||||
[TestCase("JsonSerializer.Serialize(meeting, contract)", TestName = "Writing with the contract")]
|
||||
[TestCase("element.Deserialize(contract)", TestName = "Reading a JSON element with the contract")]
|
||||
[TestCase("client.GetFromJsonAsync<Meeting>(\"/meeting\", OPTIONS)", TestName = "Reading a web answer with options")]
|
||||
[TestCase("content.ReadFromJsonAsync(contract)", TestName = "Reading web content with the contract")]
|
||||
[TestCase("JsonContent.Create(meeting, contract)", TestName = "Creating web content with the contract")]
|
||||
[TestCase("new JsonSerializerOptions { WriteIndented = true, TypeInfoResolver = resolver }", TestName = "Options with a resolver")]
|
||||
[TestCase("new JsonSerializerOptions(JsonSerializerDefaults.Web) { TypeInfoResolver = resolver }", TestName = "Options for the web with a resolver")]
|
||||
[TestCase("JsonNode.Parse(json)", TestName = "A tree of JSON nodes")]
|
||||
public async Task JsonWithGeneratedContractsIsLeftAlone(string expression)
|
||||
{
|
||||
var reported = await AnalyzeAsync(expression);
|
||||
|
||||
Assert.That(reported, Is.Empty);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<Diagnostic>> AnalyzeAsync(string expression, string otherMembers = "")
|
||||
{
|
||||
var source =
|
||||
$$"""
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace Sample;
|
||||
|
||||
public sealed class Meeting
|
||||
{
|
||||
public string Location { get; set; } = "Cologne";
|
||||
}
|
||||
|
||||
public static class MeetingJson
|
||||
{
|
||||
private static readonly JsonSerializerOptions OPTIONS = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
|
||||
|
||||
{{otherMembers}}
|
||||
|
||||
public static object? Use(Meeting meeting, string json, JsonElement element, HttpClient client, HttpContent content, JsonTypeInfo<Meeting> contract, IJsonTypeInfoResolver resolver) => {{expression}};
|
||||
}
|
||||
""";
|
||||
|
||||
return await CompilationHarness.AnalyzeAsync(source, new JsonSourceGenerationAnalyzer());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user