Skip to content

Commit

Permalink
Merge branch 'main' into fix-output-folder-paths
Browse files Browse the repository at this point in the history
  • Loading branch information
yufeih authored Feb 20, 2025
2 parents effb052 + bbe254b commit d147648
Show file tree
Hide file tree
Showing 65 changed files with 6,639 additions and 2,013 deletions.
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<PackageVersion Include="System.Composition" Version="9.0.2" />
<PackageVersion Include="System.Formats.Asn1" Version="9.0.2" />
<PackageVersion Include="System.Text.Json" Version="9.0.2" />
<PackageVersion Include="YamlDotNet" Version="15.3.0" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>

<!-- .slnx solution format is supported Microsoft.Build 17.13.9 or later. -->
Expand Down
39 changes: 38 additions & 1 deletion samples/seed/dotnet/project/Project/Inheritdoc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,41 @@ public class Class2 : Class1<bool>
public override bool TestMethod1(bool parm1, int parm2) => false;
}
}
}

// Issue #9736 #9495 #9754
public class Issue9736
{
public interface IJsonApiOptions
{
/// <summary>
/// Whether to use relative links for all resources. <c>false</c> by default.
/// </summary>
/// <example>
/// <code><![CDATA[
/// options.UseRelativeLinks = true;
/// ]]></code>
/// <code><![CDATA[
/// {
/// "type": "articles",
/// "id": "4309",
/// "relationships": {
/// "author": {
/// "links": {
/// "self": "/api/shopping/articles/4309/relationships/author",
/// "related": "/api/shopping/articles/4309/author"
/// }
/// }
/// }
/// }
/// ]]></code>
/// </example>
bool UseRelativeLinks { get; }
}

public sealed class JsonApiOptions : IJsonApiOptions
{
/// <inheritdoc />
public bool UseRelativeLinks { get; set; }
}
}
}
7 changes: 2 additions & 5 deletions src/Docfx.Dotnet/Parsers/XmlComment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,6 @@ public static XmlComment Parse(string xml, XmlCommentParserContext context = nul
}
try
{
// Format xml with indentation.
// It's needed to fix issue (https://github.com/dotnet/docfx/issues/9736)
xml = XElement.Parse(xml).ToString(SaveOptions.None);

return new XmlComment(xml, context ?? new());
}
catch (XmlException)
Expand Down Expand Up @@ -173,7 +169,8 @@ private void ResolveCode(XDocument doc, XmlCommentParserContext context)

code.SetAttributeValue("class", $"lang-{lang}");

if (node.PreviousNode is null)
if (node.PreviousNode is null
|| node.PreviousNode is XText xText && xText.Value == $"\n{indent}")
{
// Xml writer formats <pre><code> with unintended identation
// when there is no preceeding text node.
Expand Down
10 changes: 8 additions & 2 deletions src/Docfx.YamlSerialization/Helpers/ReflectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ internal static class ReflectionExtensions
/// Determines whether the specified type has a default constructor.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="allowPrivateConstructors">Allow private constructor.</param>
/// <returns>
/// <c>true</c> if the type has a default constructor; otherwise, <c>false</c>.
/// </returns>
public static bool HasDefaultConstructor(this Type type)
public static bool HasDefaultConstructor(this Type type, bool allowPrivateConstructors)
{
return type.IsValueType || type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null) != null;
var bindingFlags = BindingFlags.Public | BindingFlags.Instance;
if (allowPrivateConstructors)
{
bindingFlags |= BindingFlags.NonPublic;
}
return type.IsValueType || type.GetConstructor(bindingFlags, null, Type.EmptyTypes, null) != null;
}

public static IEnumerable<PropertyInfo> GetPublicProperties(this Type type)
Expand Down
83 changes: 83 additions & 0 deletions src/Docfx.YamlSerialization/Helpers/TypeConverterCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using YamlDotNet.Serialization;

namespace Docfx.YamlSerialization.Helpers;

/// <summary>
/// A cache / map for <see cref="IYamlTypeConverter"/> instances.
/// </summary>
/// <remarks>
/// This class is copied from following YamlDotNet implementation.
/// https://github.com/aaubry/YamlDotNet/blob/master/YamlDotNet/Serialization/Utilities/TypeConverterCache.cs
/// </remarks>
internal sealed class TypeConverterCache
{
private readonly IYamlTypeConverter[] typeConverters;
private readonly ConcurrentDictionary<Type, (bool HasMatch, IYamlTypeConverter? TypeConverter)> cache = new();

public TypeConverterCache(IEnumerable<IYamlTypeConverter>? typeConverters)
: this(typeConverters?.ToArray() ?? [])
{
}

public TypeConverterCache(IYamlTypeConverter[] typeConverters)
{
this.typeConverters = typeConverters;
}

/// <summary>
/// Returns the first <see cref="IYamlTypeConverter"/> that accepts the given type.
/// </summary>
/// <param name="type">The <see cref="Type"/> to lookup.</param>
/// <param name="typeConverter">The <see cref="IYamlTypeConverter" /> that accepts this type or <see langword="false" /> if no converter is found.</param>
/// <returns><see langword="true"/> if a type converter was found; <see langword="false"/> otherwise.</returns>
public bool TryGetConverterForType(Type type, [NotNullWhen(true)] out IYamlTypeConverter? typeConverter)
{
var result = cache.GetOrAdd(type, static (t, tc) => LookupTypeConverter(t, tc), typeConverters);

typeConverter = result.TypeConverter;
return result.HasMatch;
}

/// <summary>
/// Returns the <see cref="IYamlTypeConverter"/> of the given type.
/// </summary>
/// <param name="converter">The type of the converter.</param>
/// <returns>The <see cref="IYamlTypeConverter"/> of the given type.</returns>
/// <exception cref="ArgumentException">If no type converter of the given type is found.</exception>
/// <remarks>
/// Note that this method searches on the type of the <see cref="IYamlTypeConverter"/> itself. If you want to find a type converter
/// that accepts a given <see cref="Type"/>, use <see cref="TryGetConverterForType(Type, out IYamlTypeConverter?)"/> instead.
/// </remarks>
public IYamlTypeConverter GetConverterByType(Type converter)
{
// Intentially avoids LINQ as this is on a hot path
foreach (var typeConverter in typeConverters)
{
if (typeConverter.GetType() == converter)
{
return typeConverter;
}
}

throw new ArgumentException($"{nameof(IYamlTypeConverter)} of type {converter.FullName} not found", nameof(converter));
}

private static (bool HasMatch, IYamlTypeConverter? TypeConverter) LookupTypeConverter(Type type, IYamlTypeConverter[] typeConverters)
{
// Intentially avoids LINQ as this is on a hot path
foreach (var typeConverter in typeConverters)
{
if (typeConverter.Accepts(type))
{
return (true, typeConverter);
}
}

return (false, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,22 @@ namespace Docfx.YamlSerialization.NodeDeserializers;

public class EmitArrayNodeDeserializer : INodeDeserializer
{
private readonly INamingConvention _enumNamingConvention;
private readonly ITypeInspector _typeDescriptor;

private static readonly MethodInfo DeserializeHelperMethod =
typeof(EmitArrayNodeDeserializer).GetMethod(nameof(DeserializeHelper))!;
private static readonly ConcurrentDictionary<Type, Func<IParser, Type, Func<IParser, Type, object?>, object?>> _funcCache =

private static readonly ConcurrentDictionary<Type, Func<IParser, Type, Func<IParser, Type, object?>, INamingConvention, ITypeInspector, object?>> _funcCache =
new();

bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer, out object? value)
public EmitArrayNodeDeserializer(INamingConvention enumNamingConvention, ITypeInspector typeDescriptor)
{
_enumNamingConvention = enumNamingConvention;
_typeDescriptor = typeDescriptor;
}

bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer)
{
if (!expectedType.IsArray)
{
Expand All @@ -26,27 +36,44 @@ bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func<IPars
}

var func = _funcCache.GetOrAdd(expectedType, AddItem);
value = func(reader, expectedType, nestedObjectDeserializer);
value = func(reader, expectedType, nestedObjectDeserializer, _enumNamingConvention, _typeDescriptor);
return true;
}

[EditorBrowsable(EditorBrowsableState.Never)]
public static TItem[] DeserializeHelper<TItem>(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer)
public static TItem[] DeserializeHelper<TItem>(
IParser reader,
Type expectedType,
Func<IParser, Type, object?> nestedObjectDeserializer,
INamingConvention enumNamingConvention,
ITypeInspector typeDescriptor)
{
var items = new List<TItem>();
EmitGenericCollectionNodeDeserializer.DeserializeHelper(reader, expectedType, nestedObjectDeserializer, items);
EmitGenericCollectionNodeDeserializer.DeserializeHelper(reader, expectedType, nestedObjectDeserializer, items, enumNamingConvention, typeDescriptor);
return items.ToArray();
}

private static Func<IParser, Type, Func<IParser, Type, object?>, object?> AddItem(Type expectedType)
private static Func<IParser, Type, Func<IParser, Type, object?>, INamingConvention, ITypeInspector, object?> AddItem(Type expectedType)
{
var dm = new DynamicMethod(string.Empty, typeof(object), [typeof(IParser), typeof(Type), typeof(Func<IParser, Type, object>)]);
var dm = new DynamicMethod(
string.Empty,
returnType: typeof(object),
parameterTypes:
[
typeof(IParser), // reader
typeof(Type), // expectedType
typeof(Func<IParser, Type, object?>), // nestedObjectDeserializer
typeof(INamingConvention), // enumNamingConvention
typeof(ITypeInspector), // typeDescriptor
]);
var il = dm.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Ldarg_3);
il.Emit(OpCodes.Ldarg_S, (byte)4);
il.Emit(OpCodes.Call, DeserializeHelperMethod.MakeGenericMethod(expectedType.GetElementType()!));
il.Emit(OpCodes.Ret);
return (Func<IParser, Type, Func<IParser, Type, object?>, object?>)dm.CreateDelegate(typeof(Func<IParser, Type, Func<IParser, Type, object?>, object?>));
return (Func<IParser, Type, Func<IParser, Type, object?>, INamingConvention, ITypeInspector, object?>)dm.CreateDelegate(typeof(Func<IParser, Type, Func<IParser, Type, object?>, INamingConvention, ITypeInspector, object?>));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Serialization.Utilities;
using EditorBrowsable = System.ComponentModel.EditorBrowsableAttribute;
using EditorBrowsableState = System.ComponentModel.EditorBrowsableState;
Expand All @@ -19,15 +18,21 @@ public class EmitGenericCollectionNodeDeserializer : INodeDeserializer
private static readonly MethodInfo DeserializeHelperMethod =
typeof(EmitGenericCollectionNodeDeserializer).GetMethod(nameof(DeserializeHelper))!;
private readonly IObjectFactory _objectFactory;
private readonly Dictionary<Type, Type?> _gpCache = [];
private readonly Dictionary<Type, Action<IParser, Type, Func<IParser, Type, object?>, object?>> _actionCache = [];
private readonly INamingConvention _enumNamingConvention;
private readonly ITypeInspector _typeDescriptor;
private readonly Dictionary<Type, Type?> _gpCache =
new();
private readonly Dictionary<Type, Action<IParser, Type, Func<IParser, Type, object?>, object?, INamingConvention, ITypeInspector>> _actionCache =
new();

public EmitGenericCollectionNodeDeserializer(IObjectFactory objectFactory)
public EmitGenericCollectionNodeDeserializer(IObjectFactory objectFactory, INamingConvention enumNamingConvention, ITypeInspector typeDescriptor)
{
_objectFactory = objectFactory;
_enumNamingConvention = enumNamingConvention;
_typeDescriptor = typeDescriptor;
}

bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer, out object? value)
bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer)
{
if (!_gpCache.TryGetValue(expectedType, out var gp))
{
Expand Down Expand Up @@ -55,39 +60,58 @@ bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func<IPars
value = _objectFactory.Create(expectedType);
if (!_actionCache.TryGetValue(gp, out var action))
{
var dm = new DynamicMethod(string.Empty, typeof(void), [typeof(IParser), typeof(Type), typeof(Func<IParser, Type, object>), typeof(object)]);
var dm = new DynamicMethod(
string.Empty,
returnType: typeof(void),
[
typeof(IParser),
typeof(Type),
typeof(Func<IParser, Type, object?>),
typeof(object),
typeof(INamingConvention),
typeof(ITypeInspector)
]);

var il = dm.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Ldarg_3);
il.Emit(OpCodes.Ldarg_0); // reader
il.Emit(OpCodes.Ldarg_1); // expectedType
il.Emit(OpCodes.Ldarg_2); // nestedObjectDeserializer
il.Emit(OpCodes.Ldarg_3); // result
il.Emit(OpCodes.Castclass, typeof(ICollection<>).MakeGenericType(gp));
il.Emit(OpCodes.Ldarg_S, (byte)4); // enumNamingConvention
il.Emit(OpCodes.Ldarg_S, (byte)5); // typeDescriptor
il.Emit(OpCodes.Call, DeserializeHelperMethod.MakeGenericMethod(gp));
il.Emit(OpCodes.Ret);
action = (Action<IParser, Type, Func<IParser, Type, object?>, object?>)dm.CreateDelegate(typeof(Action<IParser, Type, Func<IParser, Type, object?>, object?>));
action = (Action<IParser, Type, Func<IParser, Type, object?>, object?, INamingConvention, ITypeInspector>)dm.CreateDelegate(typeof(Action<IParser, Type, Func<IParser, Type, object?>, object?, INamingConvention, ITypeInspector>));
_actionCache[gp] = action;
}

action(reader, expectedType, nestedObjectDeserializer, value);
action(reader, expectedType, nestedObjectDeserializer, value, _enumNamingConvention, _typeDescriptor);
return true;
}

[EditorBrowsable(EditorBrowsableState.Never)]
public static void DeserializeHelper<TItem>(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer, ICollection<TItem> result)
public static void DeserializeHelper<TItem>(
IParser reader,
Type expectedType,
Func<IParser, Type, object?> nestedObjectDeserializer,
ICollection<TItem> result,
INamingConvention enumNamingConvention,
ITypeInspector typeDescriptor)
{
reader.Consume<SequenceStart>();
while (!reader.Accept<SequenceEnd>(out _))
{
var value = nestedObjectDeserializer(reader, typeof(TItem));
if (value is not IValuePromise promise)
{
result.Add(TypeConverter.ChangeType<TItem>(value, NullNamingConvention.Instance));
result.Add(TypeConverter.ChangeType<TItem>(value, enumNamingConvention, typeDescriptor));
}
else if (result is IList<TItem> list)
{
var index = list.Count;
result.Add(default!);
promise.ValueAvailable += v => list[index] = TypeConverter.ChangeType<TItem>(v, NullNamingConvention.Instance);
promise.ValueAvailable += v => list[index] = TypeConverter.ChangeType<TItem>(v, enumNamingConvention, typeDescriptor);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public EmitGenericDictionaryNodeDeserializer(IObjectFactory objectFactory)
_objectFactory = objectFactory;
}

bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer, out object? value)
bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer, out object? value, ObjectDeserializer rootDeserializer)
{
if (!_gpCache.TryGetValue(expectedType, out var gp))
{
Expand Down
Loading

0 comments on commit d147648

Please sign in to comment.