Skip to content

Commit

Permalink
Merge branch 'release/9.0' into backport/pr-109008-to-release/9.0
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffschwMSFT authored Oct 18, 2024
2 parents 1cad22a + 83d152c commit 1f392cd
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 43 deletions.
9 changes: 7 additions & 2 deletions src/coreclr/md/compiler/importhelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3174,6 +3174,8 @@ ImportHelper::CreateAssemblyRefFromAssembly(
mdAssemblyRef tkAssemRef;
HRESULT hr = S_OK;
StrongNameToken token;
const void *pbToken = NULL;
ULONG cbToken = 0;
ULONG i;

// Set output to Nil.
Expand All @@ -3193,6 +3195,9 @@ ImportHelper::CreateAssemblyRefFromAssembly(
IfFailGo(StrongNameTokenFromPublicKey((BYTE*)pbPublicKey,
cbPublicKey,
&token));

pbToken = &token;
cbToken = StrongNameToken::SIZEOF_TOKEN;
}
else
_ASSERTE(!IsAfPublicKey(dwFlags));
Expand All @@ -3209,8 +3214,8 @@ ImportHelper::CreateAssemblyRefFromAssembly(
continue;

// See if the AssemblyRef already exists in the emit scope.
hr = FindAssemblyRef(pMiniMdEmit, szName, szLocale, &token,
StrongNameToken::SIZEOF_TOKEN, usMajorVersion, usMinorVersion,
hr = FindAssemblyRef(pMiniMdEmit, szName, szLocale, pbToken,
cbToken, usMajorVersion, usMinorVersion,
usBuildNumber, usRevisionNumber, dwFlags,
&tkAssemRef);
if (hr == CLDB_E_RECORD_NOTFOUND)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ public JsonSchema() { }

public bool IsTrue => _trueOrFalse is true;
public bool IsFalse => _trueOrFalse is false;

/// <summary>
/// Per the JSON schema core specification section 4.3
/// (https://json-schema.org/draft/2020-12/json-schema-core#name-json-schema-documents)
/// A JSON schema must either be an object or a boolean.
/// We represent false and true schemas using this flag.
/// It is not possible to specify keywords in boolean schemas.
/// </summary>
private readonly bool? _trueOrFalse;

public string? Ref { get => _ref; set { VerifyMutable(); _ref = value; } }
Expand Down Expand Up @@ -95,6 +103,7 @@ public int KeywordCount
{
if (_trueOrFalse != null)
{
// Boolean schemas admit no keywords
return 0;
}

Expand Down Expand Up @@ -129,6 +138,7 @@ public void MakeNullable()
{
if (_trueOrFalse != null)
{
// boolean schemas do not admit type keywords.
return;
}

Expand Down Expand Up @@ -260,6 +270,23 @@ JsonNode CompleteSchema(JsonNode schema)
}
}

/// <summary>
/// If the schema is boolean, replaces it with a semantically
/// equivalent object schema that allows appending keywords.
/// </summary>
public static void EnsureMutable(ref JsonSchema schema)
{
switch (schema._trueOrFalse)
{
case false:
schema = new JsonSchema { Not = True };
break;
case true:
schema = new JsonSchema();
break;
}
}

private static ReadOnlySpan<JsonSchemaType> s_schemaValues =>
[
// NB the order of these values influences order of types in the rendered schema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
Expand Down Expand Up @@ -76,9 +77,12 @@ private static JsonSchema MapJsonSchemaCore(
{
Debug.Assert(typeInfo.IsConfigured);

if (cacheResult && state.TryPushType(typeInfo, propertyInfo, out string? existingJsonPointer))
JsonSchemaExporterContext exporterContext = state.CreateContext(typeInfo, propertyInfo, parentPolymorphicTypeInfo);

if (cacheResult && typeInfo.Kind is not JsonTypeInfoKind.None &&
state.TryGetExistingJsonPointer(exporterContext, out string? existingJsonPointer))
{
// We're generating the schema of a recursive type, return a reference pointing to the outermost schema.
// The schema context has already been generated in the schema document, return a reference to it.
return CompleteSchema(ref state, new JsonSchema { Ref = existingJsonPointer });
}

Expand Down Expand Up @@ -240,6 +244,7 @@ private static JsonSchema MapJsonSchemaCore(

if (property.AssociatedParameter is { HasDefaultValue: true } parameterInfo)
{
JsonSchema.EnsureMutable(ref propertySchema);
propertySchema.DefaultValue = JsonSerializer.SerializeToNode(parameterInfo.DefaultValue, property.JsonTypeInfo);
propertySchema.HasDefaultValue = true;
}
Expand Down Expand Up @@ -363,17 +368,12 @@ JsonSchema CompleteSchema(ref GenerationState state, JsonSchema schema)
{
schema.MakeNullable();
}

if (cacheResult)
{
state.PopGeneratedType();
}
}

if (state.ExporterOptions.TransformSchemaNode != null)
{
// Prime the schema for invocation by the JsonNode transformer.
schema.ExporterContext = state.CreateContext(typeInfo, propertyInfo, parentPolymorphicTypeInfo);
schema.ExporterContext = exporterContext;
}

return schema;
Expand Down Expand Up @@ -408,7 +408,7 @@ private static bool IsPolymorphicTypeThatSpecifiesItselfAsDerivedType(JsonTypeIn
private readonly ref struct GenerationState(JsonSerializerOptions options, JsonSchemaExporterOptions exporterOptions)
{
private readonly List<string> _currentPath = [];
private readonly List<(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo, int depth)> _generationStack = [];
private readonly Dictionary<(JsonTypeInfo, JsonPropertyInfo?), string[]> _generated = new();

public int CurrentDepth => _currentPath.Count;
public JsonSerializerOptions Options { get; } = options;
Expand All @@ -431,77 +431,75 @@ public void PopSchemaNode()
}

/// <summary>
/// Pushes the current type/property to the generation stack or returns a JSON pointer if the type is recursive.
/// Registers the current schema node generation context; if it has already been generated return a JSON pointer to its location.
/// </summary>
public bool TryPushType(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo, [NotNullWhen(true)] out string? existingJsonPointer)
public bool TryGetExistingJsonPointer(in JsonSchemaExporterContext context, [NotNullWhen(true)] out string? existingJsonPointer)
{
foreach ((JsonTypeInfo otherTypeInfo, JsonPropertyInfo? otherPropertyInfo, int depth) in _generationStack)
(JsonTypeInfo TypeInfo, JsonPropertyInfo? PropertyInfo) key = (context.TypeInfo, context.PropertyInfo);
#if NET
ref string[]? pathToSchema = ref CollectionsMarshal.GetValueRefOrAddDefault(_generated, key, out bool exists);
#else
bool exists = _generated.TryGetValue(key, out string[]? pathToSchema);
#endif
if (exists)
{
if (typeInfo == otherTypeInfo && propertyInfo == otherPropertyInfo)
{
existingJsonPointer = FormatJsonPointer(_currentPath, depth);
return true;
}
existingJsonPointer = FormatJsonPointer(pathToSchema);
return true;
}

_generationStack.Add((typeInfo, propertyInfo, CurrentDepth));
#if NET
pathToSchema = context._path;
#else
_generated[key] = context._path;
#endif
existingJsonPointer = null;
return false;
}

public void PopGeneratedType()
{
Debug.Assert(_generationStack.Count > 0);
_generationStack.RemoveAt(_generationStack.Count - 1);
}

public JsonSchemaExporterContext CreateContext(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo, JsonTypeInfo? baseTypeInfo)
{
return new JsonSchemaExporterContext(typeInfo, propertyInfo, baseTypeInfo, _currentPath.ToArray());
return new JsonSchemaExporterContext(typeInfo, propertyInfo, baseTypeInfo, [.. _currentPath]);
}

private static string FormatJsonPointer(List<string> currentPathList, int depth)
private static string FormatJsonPointer(ReadOnlySpan<string> path)
{
Debug.Assert(0 <= depth && depth < currentPathList.Count);

if (depth == 0)
if (path.IsEmpty)
{
return "#";
}

using ValueStringBuilder sb = new(initialCapacity: depth * 10);
using ValueStringBuilder sb = new(initialCapacity: path.Length * 10);
sb.Append('#');

for (int i = 0; i < depth; i++)
foreach (string segment in path)
{
ReadOnlySpan<char> segment = currentPathList[i].AsSpan();
ReadOnlySpan<char> span = segment.AsSpan();
sb.Append('/');

do
{
// Per RFC 6901 the characters '~' and '/' must be escaped.
int pos = segment.IndexOfAny('~', '/');
int pos = span.IndexOfAny('~', '/');
if (pos < 0)
{
sb.Append(segment);
sb.Append(span);
break;
}

sb.Append(segment.Slice(0, pos));
sb.Append(span.Slice(0, pos));

if (segment[pos] == '~')
if (span[pos] == '~')
{
sb.Append("~0");
}
else
{
Debug.Assert(segment[pos] == '/');
Debug.Assert(span[pos] == '/');
sb.Append("~1");
}

segment = segment.Slice(pos + 1);
span = span.Slice(pos + 1);
}
while (!segment.IsEmpty);
while (!span.IsEmpty);
}

return sb.ToString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace System.Text.Json.Schema
/// </summary>
public readonly struct JsonSchemaExporterContext
{
private readonly string[] _path;
internal readonly string[] _path;

internal JsonSchemaExporterContext(
JsonTypeInfo typeInfo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,62 @@ public static IEnumerable<ITestData> GetTestDataCore()
""",
Options: new() { TreatNullObliviousAsNonNullable = true });

SimpleRecord recordValue = new(42, "str", true, 3.14);
yield return new TestData<PocoWithNonRecursiveDuplicateOccurrences>(
Value: new() { Value1 = recordValue, Value2 = recordValue, ArrayValue = [recordValue], ListValue = [recordValue] },
ExpectedJsonSchema: """
{
"type": ["object","null"],
"properties": {
"Value1": {
"type": "object",
"properties": {
"X": { "type": "integer" },
"Y": { "type": "string" },
"Z": { "type": "boolean" },
"W": { "type": "number" }
},
"required": ["X", "Y", "Z", "W"]
},
/* The same type on a different property is repeated to
account for potential metadata resolved from attributes. */
"Value2": {
"type": "object",
"properties": {
"X": { "type": "integer" },
"Y": { "type": "string" },
"Z": { "type": "boolean" },
"W": { "type": "number" }
},
"required": ["X", "Y", "Z", "W"]
},
/* This collection element is the first occurrence
of the type without contextual metadata. */
"ListValue": {
"type": "array",
"items": {
"type": ["object","null"],
"properties": {
"X": { "type": "integer" },
"Y": { "type": "string" },
"Z": { "type": "boolean" },
"W": { "type": "number" }
},
"required": ["X", "Y", "Z", "W"]
}
},
/* This collection element is the second occurrence
of the type which points to the first occurrence. */
"ArrayValue": {
"type": "array",
"items": {
"$ref": "#/properties/ListValue/items"
}
}
}
}
""");

yield return new TestData<PocoWithDescription>(
Value: new() { X = 42 },
ExpectedJsonSchema: """
Expand Down Expand Up @@ -1034,6 +1090,18 @@ public static IEnumerable<ITestData> GetTestDataCore()
}
""");

yield return new TestData<ClassWithOptionalObjectParameter>(
Value: new(value: null),
AdditionalValues: [new(true), new(42), new(""), new(new object()), new(Array.Empty<int>())],
ExpectedJsonSchema: """
{
"type": ["object","null"],
"properties": {
"Value": { "default": null }
}
}
""");

// Collection types
yield return new TestData<int[]>([1, 2, 3], ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"integer"}}""");
yield return new TestData<List<bool>>([false, true, false], ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"boolean"}}""");
Expand Down Expand Up @@ -1214,6 +1282,14 @@ public class PocoWithRecursiveDictionaryValue
public Dictionary<string, PocoWithRecursiveDictionaryValue> Children { get; init; } = new();
}

public class PocoWithNonRecursiveDuplicateOccurrences
{
public SimpleRecord Value1 { get; set; }
public SimpleRecord Value2 { get; set; }
public List<SimpleRecord> ListValue { get; set; }
public SimpleRecord[] ArrayValue { get; set; }
}

[Description("The type description")]
public class PocoWithDescription
{
Expand Down Expand Up @@ -1441,6 +1517,11 @@ public class ClassWithJsonPointerEscapablePropertyNames
public PocoWithRecursiveMembers Value { get; set; }
}

public class ClassWithOptionalObjectParameter(object? value = null)
{
public object? Value { get; } = value;
}

public readonly struct StructDictionary<TKey, TValue>(IEnumerable<KeyValuePair<TKey, TValue>> values)
: IReadOnlyDictionary<TKey, TValue>
where TKey : notnull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Serialization.Tests;
using System.Xml.Linq;
using Json.Schema;
using Xunit;
using Xunit.Sdk;
Expand Down Expand Up @@ -90,6 +91,13 @@ public void UnsupportedType_ReturnsExpectedSchema(Type type)
Assert.Equal(""""{"$comment":"Unsupported .NET type","not":true}"""", schema.ToJsonString());
}

[Fact]
public void CanGenerateXElementSchema()
{
JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(typeof(XElement));
Assert.True(schema.ToJsonString().Length < 100_000);
}

[Fact]
public void TypeWithDisallowUnmappedMembers_AdditionalPropertiesFailValidation()
{
Expand Down
Loading

0 comments on commit 1f392cd

Please sign in to comment.