Skip to content

feature/type-declarations #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>9.4.0</Version>
<PackageVersion>9.4.0</PackageVersion>
<AssemblyVersion>9.4.0</AssemblyVersion>
<Version>9.5.0</Version>
<PackageVersion>9.5.0</PackageVersion>
<AssemblyVersion>9.5.0</AssemblyVersion>
</PropertyGroup>
</Project>
583 changes: 583 additions & 0 deletions OnixLabs.Core.UnitTests/Reflection/TypeExtensionTests.cs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions OnixLabs.Core/Extensions.Object.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public static string ToRecordString(this object? value)
StringBuilder builder = new();
IEnumerable<PropertyInfo> properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

builder.Append(type.GetName());
builder.Append(type.GetCSharpTypeDeclaration());

if (properties.IsEmpty())
return builder.Append(ObjectEmptyBrackets).ToString();
Expand Down Expand Up @@ -160,7 +160,7 @@ public static string ToRecordString(this object? value)
}
catch
{
return string.Concat(type.GetName(), ObjectEmptyBrackets);
return string.Concat(type.GetCSharpTypeDeclaration(), ObjectEmptyBrackets);
}
}

Expand Down
233 changes: 233 additions & 0 deletions OnixLabs.Core/Reflection/CSharpTypeDeclarationFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// Copyright 2020 ONIXLabs
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Text;
using OnixLabs.Core.Text;

namespace OnixLabs.Core.Reflection;

/// <summary>
/// Represents a formatter for C# type declarations.
/// <remarks>
/// There are some limitations as to what this formatter is capable of producing; for example, nullability state information
/// for nullable reference types, and <see cref="ValueTuple"/> custom names are not available within a <see cref="Type"/>
/// instance, and therefore cannot be produced by this type declaration formatter.
/// </remarks>
/// </summary>
internal static class CSharpTypeDeclarationFormatter
{
private const char NullableTypeIdentifier = '?';
private const char GenericTypeIdentifierMarker = '`';
private const char GenericTypeOpenBracket = '<';
private const char GenericTypeCloseBracket = '>';
private const char ValueTupleOpenParenthesis = '(';
private const char ValueTupleCloseParenthesis = ')';
private const string TypeSeparator = ", ";
private const string ValueTupleItemName = " Item";
private const string TypeNullExceptionMessage = "Type must not be null.";

private static readonly Dictionary<Type, string> TypeAliases = new()
{
[typeof(byte)] = "byte",
[typeof(sbyte)] = "sbyte",
[typeof(short)] = "short",
[typeof(ushort)] = "ushort",
[typeof(int)] = "int",
[typeof(uint)] = "uint",
[typeof(long)] = "long",
[typeof(ulong)] = "ulong",
[typeof(nint)] = "nint",
[typeof(nuint)] = "nuint",
[typeof(float)] = "float",
[typeof(double)] = "double",
[typeof(decimal)] = "decimal",
[typeof(object)] = "object",
[typeof(bool)] = "bool",
[typeof(char)] = "char",
[typeof(string)] = "string",
[typeof(void)] = "void"
};

/// <summary>
/// Gets the type declaration for the current <see cref="Type"/> instance.
/// <remarks>
/// Depending on the specified <see cref="TypeDeclarationFlags"/>, this method is capable or returning type declarations including
/// simple type names, namespace qualified types names, aliased types names, nullable shorthand notation, generic arguments, and value tuples.
/// </remarks>
/// </summary>
/// <param name="type">The current <see cref="Type"/> instance from which to obtain the type declaration.</param>
/// <param name="flags">The flags that specify how the type declaration should be formatted.</param>
/// <returns>Returns the type declaration for the current <see cref="Type"/> instance.</returns>
public static string GetTypeDeclaration(Type type, TypeDeclarationFlags flags)
{
RequireNotNull(type, TypeNullExceptionMessage, nameof(type));

Type unwrappedType = ConditionallyUnwrapNullableType(type, flags);
StringBuilder builder = new();

if (CanFormatValueTupleType(unwrappedType, flags))
FormatValueTupleType(unwrappedType, builder, flags);

else if (CanFormatGenericType(unwrappedType, flags))
FormatGenericType(unwrappedType, builder, flags);

else FormatTypeName(unwrappedType, builder, flags);

FormatNullableShorthandNotation(type, builder, flags);

return builder.ToString();
}

/// <summary>
/// Conditionally unwraps a <see cref="Nullable{T}"/> type, if the <see cref="TypeDeclarationFlags.UseNullableShorthandTypeNames"/> flag is set.
/// </summary>
/// <param name="type">The potential <see cref="Nullable{T}"/> type to unwrap.</param>
/// <param name="flags">The flags that specify how the type declaration should be formatted.</param>
/// <returns>
/// Returns the underlying type, if the specified type is <see cref="Nullable{T}"/>, and the
/// <see cref="TypeDeclarationFlags.UseNullableShorthandTypeNames"/> flag is set; otherwise, returns the current <see cref="Type"/>.
/// </returns>
private static Type ConditionallyUnwrapNullableType(Type type, TypeDeclarationFlags flags)
{
if ((flags & TypeDeclarationFlags.UseNullableShorthandTypeNames) is not 0)
return Nullable.GetUnderlyingType(type) ?? type;

return type;
}

/// <summary>
/// Determines whether the specified type is a generic type, and whether the <see cref="TypeDeclarationFlags.UseGenericTypeArguments"/> flag is set.
/// </summary>
/// <param name="type">The type to check is potentially generic.</param>
/// <param name="flags">The flags that specify how the type declaration should be formatted.</param>
/// <returns>Returns true if the specified type is a generic type, and the <see cref="TypeDeclarationFlags.UseGenericTypeArguments"/> flag is set; otherwise, false.</returns>
private static bool CanFormatGenericType(Type type, TypeDeclarationFlags flags)
{
bool useGenericTypeArguments = (flags & TypeDeclarationFlags.UseGenericTypeArguments) is not 0;
return useGenericTypeArguments && type.IsGenericType;
}

/// <summary>
/// Determines whether the specified type is a multi-argument value-tuple type, and whether the
/// <see cref="TypeDeclarationFlags.UseValueTupleSyntax"/> or <see cref="TypeDeclarationFlags.UseValueTupleNames"/> flag is set.
/// </summary>
/// <param name="type">The type to check is potentially a multi-argument value-tuple type.</param>
/// <param name="flags">The flags that specify how the type declaration should be formatted.</param>
/// <returns>
/// Returns true if the specified type is a multi-argument value-tuple type, and whether the
/// <see cref="TypeDeclarationFlags.UseValueTupleSyntax"/> or <see cref="TypeDeclarationFlags.UseValueTupleNames"/> flag is set; otherwise, false.
/// </returns>
private static bool CanFormatValueTupleType(Type type, TypeDeclarationFlags flags)
{
bool useTupleSyntax = (flags & TypeDeclarationFlags.UseValueTupleSyntax) is not 0;
bool useTupleNames = (flags & TypeDeclarationFlags.UseValueTupleNames) is not 0;

return (useTupleSyntax || useTupleNames)
&& type.Name.StartsWith(nameof(ValueTuple))
&& type.GenericTypeArguments.Length > 1;
}

/// <summary>
/// Formats the specified type as a value-tuple.
/// </summary>
/// <param name="type">The type to format.</param>
/// <param name="builder">The <see cref="StringBuilder"/> to which the type information will be appended.</param>
/// <param name="flags">The flags that specify how the type declaration should be formatted.</param>
private static void FormatValueTupleType(Type type, StringBuilder builder, TypeDeclarationFlags flags)
{
bool useTupleNames = (flags & TypeDeclarationFlags.UseValueTupleNames) is not 0;

builder.Append(ValueTupleOpenParenthesis);
FormatTypeArguments(type.GetGenericArguments(), builder, flags, useTupleNames);
builder.Append(ValueTupleCloseParenthesis);
}

/// <summary>
/// Formats the specified type as a generic type.
/// </summary>
/// <param name="type">The type to format.</param>
/// <param name="builder">The <see cref="StringBuilder"/> to which the type information will be appended.</param>
/// <param name="flags">The flags that specify how the type declaration should be formatted.</param>
private static void FormatGenericType(Type type, StringBuilder builder, TypeDeclarationFlags flags)
{
FormatTypeName(type, builder, flags);
builder.Append(GenericTypeOpenBracket);
FormatTypeArguments(type.GetGenericArguments(), builder, flags, useTupleNames: false);
builder.Append(GenericTypeCloseBracket);
}

/// <summary>
/// Formats the specified type as a type alias, namespace qualified name, or simple name.
/// </summary>
/// <param name="type">The type to format.</param>
/// <param name="builder">The <see cref="StringBuilder"/> to which the type information will be appended.</param>
/// <param name="flags">The flags that specify how the type declaration should be formatted.</param>
private static void FormatTypeName(Type type, StringBuilder builder, TypeDeclarationFlags flags)
{
bool useAliasedTypeNames = (flags & TypeDeclarationFlags.UseAliasedTypeNames) is not 0;
bool useNamespaceQualifiedTypeNames = (flags & TypeDeclarationFlags.UseNamespaceQualifiedTypeNames) is not 0;

if (useAliasedTypeNames && TypeAliases.TryGetValue(type, out string? alias))
{
builder.Append(alias);
return;
}

if (useNamespaceQualifiedTypeNames)
{
builder.Append((type.FullName ?? type.Name).SubstringBeforeFirst(GenericTypeIdentifierMarker));
return;
}

builder.Append(type.Name.SubstringBeforeFirst(GenericTypeIdentifierMarker));
}

/// <summary>
/// Formats the specified type's generic argument types.
/// </summary>
/// <param name="arguments">The argument types to format.</param>
/// <param name="builder">The <see cref="StringBuilder"/> to which the type information will be appended.</param>
/// <param name="flags">The flags that specify how the type declaration should be formatted.</param>
/// <param name="useTupleNames">Specifies whether tuple names should be formatted.</param>
private static void FormatTypeArguments(Type[] arguments, StringBuilder builder, TypeDeclarationFlags flags, bool useTupleNames)
{
for (int index = 0; index < arguments.Length; index++)
{
builder.Append(GetTypeDeclaration(arguments[index], flags));

if (useTupleNames)
builder
.Append(ValueTupleItemName)
.Append(index + 1);

builder.Append(TypeSeparator);
}

builder.TrimEnd(TypeSeparator);
}

/// <summary>
/// Formats the type using nullable shorthand notation.
/// </summary>
/// <param name="type">The type to format.</param>
/// <param name="builder">The <see cref="StringBuilder"/> to which the type information will be appended.</param>
/// <param name="flags">The flags that specify how the type declaration should be formatted.</param>
private static void FormatNullableShorthandNotation(Type type, StringBuilder builder, TypeDeclarationFlags flags)
{
if ((flags & TypeDeclarationFlags.UseNullableShorthandTypeNames) is not 0 && Nullable.GetUnderlyingType(type) is not null)
builder.Append(NullableTypeIdentifier);
}
}
14 changes: 14 additions & 0 deletions OnixLabs.Core/Reflection/Extensions.Type.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public static class TypeExtensions
/// <param name="type">The current <see cref="Type"/> instance from which to obtain the formatted type name.</param>
/// <param name="flags">The type name flags that will be used to format the type name.</param>
/// <returns>Returns the formatted type name from the current <see cref="Type"/> instance.</returns>
[Obsolete("This method has been replaced with GetCSharpTypeDeclaration and will be removed in version 10.0.0")]
public static string GetName(this Type type, TypeNameFlags flags = default)
{
RequireNotNull(type, TypeNullExceptionMessage, nameof(type));
Expand Down Expand Up @@ -66,4 +67,17 @@ public static string GetName(this Type type, TypeNameFlags flags = default)
/// <returns>Returns the simple type name from the current <see cref="Type"/> instance.</returns>
private static string GetName(this Type type, bool useFullName) =>
(useFullName ? type.FullName ?? type.Name : type.Name).SubstringBeforeFirst(GenericTypeIdentifierMarker);

/// <summary>
/// Gets the type declaration for the current <see cref="Type"/> instance.
/// <remarks>
/// Depending on the specified <see cref="TypeDeclarationFlags"/>, this method is capable or returning type declarations including
/// simple type names, namespace qualified types names, aliased types names, nullable shorthand notation, generic arguments, and value tuples.
/// </remarks>
/// </summary>
/// <param name="type">The current <see cref="Type"/> instance from which to obtain the type declaration.</param>
/// <param name="flags">The flags that specify how the type declaration should be formatted.</param>
/// <returns>Returns the type declaration for the current <see cref="Type"/> instance.</returns>
public static string GetCSharpTypeDeclaration(this Type type, TypeDeclarationFlags flags = default) =>
CSharpTypeDeclarationFormatter.GetTypeDeclaration(type, flags);
}
82 changes: 82 additions & 0 deletions OnixLabs.Core/Reflection/TypeDeclarationFlags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2020 ONIXLabs
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;

namespace OnixLabs.Core.Reflection;

/// <summary>
/// Specifies flags that control how a type name is formatted.
/// </summary>
[Flags]
public enum TypeDeclarationFlags
{
/// <summary>
/// Specifies that no type name arguments are applied.
/// <remarks>Only simple CLR type names will be used, and excludes generic type arguments and tuple syntax.</remarks>
/// </summary>
None = default,

/// <summary>
/// Specifies that namespace qualified CLR type names will be used, where applicable.
/// <remarks>
/// If the namespace qualified CLR type name is not available, then this will use the type's simple CLR type name instead.
/// </remarks>
/// </summary>
UseNamespaceQualifiedTypeNames = 1 << 0,

/// <summary>
/// Specifies that type alias names will be used, where applicable.
/// <remarks>
/// This flag supersedes the <see cref="UseNamespaceQualifiedTypeNames"/> flag, therefore if a type alias name is not available, then the namespace
/// qualified CLR type name will be used if <see cref="UseNamespaceQualifiedTypeNames"/> is set; otherwise, the type's simple CLR type name will be used.
/// </remarks>
/// </summary>
UseAliasedTypeNames = 1 << 1,

/// <summary>
/// Specifies that <see cref="Nullable{T}"/> types will be formatted using nullable shorthand syntax.
/// <remarks>
/// This flag supersedes the <see cref="UseGenericTypeArguments"/> flag.
/// </remarks>
/// </summary>
UseNullableShorthandTypeNames = 1 << 2,

/// <summary>
/// Specifies that if a type is generic, it should be formatted with its generic type arguments.
/// </summary>
UseGenericTypeArguments = 1 << 3,

/// <summary>
/// Specifies that types of <see cref="ValueTuple"/> will be formatted using tuple syntax.
/// <remarks>This flag supersedes the <see cref="UseGenericTypeArguments"/> flag.</remarks>
/// </summary>
UseValueTupleSyntax = 1 << 4,

/// <summary>
/// Specifies that types of <see cref="ValueTuple"/> will be formatted using tuple names, where applicable.
/// <remarks>This flag supersedes the <see cref="UseValueTupleSyntax"/> and <see cref="UseGenericTypeArguments"/> flags.</remarks>
/// </summary>
UseValueTupleNames = 1 << 5,

/// <summary>
/// Specifies that all type name flags are set.
/// </summary>
All = UseNamespaceQualifiedTypeNames
| UseAliasedTypeNames
| UseNullableShorthandTypeNames
| UseGenericTypeArguments
| UseValueTupleSyntax
| UseValueTupleNames
}
2 changes: 1 addition & 1 deletion OnixLabs.Core/Reflection/TypeNameFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace OnixLabs.Core.Reflection;
/// <summary>
/// Specifies flags that control how a type name is formatted.
/// </summary>
[Flags]
[Flags, Obsolete("This enumeration has been replaced with TypeDeclarationFlags and will be removed in version 10.0.0")]
public enum TypeNameFlags
{
/// <summary>
Expand Down
Loading