Skip to content

Commit

Permalink
introduce TotalComplexity
Browse files Browse the repository at this point in the history
  • Loading branch information
adamsitnik committed Feb 7, 2024
1 parent 745e7bb commit ee43e71
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 9 deletions.
78 changes: 69 additions & 9 deletions src/libraries/Common/src/System/Reflection/Metadata/TypeName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ sealed class TypeName
UnderlyingType = underlyingType;
ContainingType = containingType;
_genericArguments = genericTypeArguments;
TotalComplexity = GetTotalComplexity(underlyingType, containingType, genericTypeArguments);
}

/// <summary>
Expand All @@ -52,6 +53,15 @@ public string AssemblyQualifiedName
/// </summary>
public AssemblyName? AssemblyName { get; } // TODO: AssemblyName is mutable, are we fine with that? Does it not offer too much?

/// <summary>
/// If this type is a nested type (see <see cref="IsNestedType"/>), gets
/// the containing type. If this type is not a nested type, returns null.
/// </summary>
/// <remarks>
/// For example, given "Namespace.Containing+Nested", unwraps the outermost type and returns "Namespace.Containing".
/// </remarks>
public TypeName? ContainingType { get; }

/// <summary>
/// Returns true if this type represents any kind of array, regardless of the array's
/// rank or its bounds.
Expand Down Expand Up @@ -110,19 +120,39 @@ public string AssemblyQualifiedName
public bool IsVariableBoundArrayType => _rankOrModifier > 1;

/// <summary>
/// If this type is a nested type (see <see cref="IsNestedType"/>), gets
/// the containing type. If this type is not a nested type, returns null.
/// The name of this type, without the namespace and the assembly name; e.g., "Int32".
/// Nested types are represented without a '+'; e.g., "MyNamespace.MyType+NestedType" is just "NestedType".
/// </summary>
/// <remarks>
/// For example, given "Namespace.Containing+Nested", unwraps the outermost type and returns "Namespace.Containing".
/// </remarks>
public TypeName? ContainingType { get; }
public string Name { get; }

/// <summary>
/// The name of this type, including namespace, but without the assembly name; e.g., "System.Int32".
/// Nested types are represented with a '+'; e.g., "MyNamespace.MyType+NestedType".
/// Represents the total amount of work that needs to be performed to fully inspect
/// this instance, including any generic arguments or underlying types.
/// </summary>
public string Name { get; }
/// <remarks>
/// <para>There's not really a parallel concept to this in reflection. Think of it
/// as the total number of <see cref="TypeName"/> instances that would be created if
/// you were to totally deconstruct this instance and visit each intermediate <see cref="TypeName"/>
/// that occurs as part of deconstruction.</para>
/// <para>"int" and "Person" each have complexities of 1 because they're standalone types.</para>
/// <para>"int[]" has a complexity of 2 because to fully inspect it involves inspecting the
/// array type itself, <em>plus</em> unwrapping the underlying type ("int") and inspecting that.</para>
/// <para>
/// "Dictionary&lt;string, List&lt;int[][]&gt;&gt;" has complexity 8 because fully visiting it
/// involves inspecting 8 <see cref="TypeName"/> instances total:
/// <list type="bullet">
/// <item>Dictionary&lt;string, List&lt;int[][]&gt;&gt; (the original type)</item>
/// <item>Dictionary`2 (the generic type definition)</item>
/// <item>string (a type argument of Dictionary)</item>
/// <item>List&lt;int[][]&gt; (a type argument of Dictionary)</item>
/// <item>List`1 (the generic type definition)</item>
/// <item>int[][] (a type argument of List)</item>
/// <item>int[] (the underlying type of int[][])</item>
/// <item>int (the underlying type of int[])</item>
/// </list>
/// </para>
/// </remarks>
public int TotalComplexity { get; }

/// <summary>
/// If this type is not an elemental type (see <see cref="IsElementalType"/>), gets
Expand Down Expand Up @@ -168,5 +198,35 @@ public TypeName[] GetGenericArguments()
=> _genericArguments is not null
? (TypeName[])_genericArguments.Clone() // we return a copy on purpose, to not allow for mutations. TODO: consider returning a ROS
: Array.Empty<TypeName>(); // TODO: should we throw (Levi's parser throws InvalidOperationException in such case), Type.GetGenericArguments just returns an empty array

private static int GetTotalComplexity(TypeName? underlyingType, TypeName? containingType, TypeName[]? genericTypeArguments)
{
int result = 1;

if (underlyingType is not null)
{
result = checked(result + underlyingType.TotalComplexity);
}

if (containingType is not null)
{
result = checked(result + containingType.TotalComplexity);
}

if (genericTypeArguments is not null)
{
// New total complexity will be the sum of the cumulative args' complexity + 2:
// - one for the generic type definition "MyGeneric`x"
// - one for the constructed type definition "MyGeneric`x[[...]]"
// - and the cumulative complexity of all the arguments
result = checked(result + 1);
foreach (TypeName genericArgument in genericTypeArguments)
{
result = checked(result + genericArgument.TotalComplexity);
}
}

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2423,6 +2423,7 @@ public sealed partial class TypeName
public bool IsUnmanagedPointerType { get { throw null; } }
public bool IsVariableBoundArrayType { get { throw null; } }
public string Name { get { throw null; } }
public int TotalComplexity { get; }
public System.Reflection.Metadata.TypeName? UnderlyingType { get { throw null; } }
public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan<char> typeName, System.Reflection.Metadata.TypeNameParserOptions? options = null) { throw null; }
public static bool TryParse(System.ReadOnlySpan<char> typeName, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Reflection.Metadata.TypeName? result, System.Reflection.Metadata.TypeNameParserOptions? options = null) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,50 @@ public void DecoratorsAreSupported(string input, string typeNameWithoutDecorator
Assert.Null(underlyingType.UnderlyingType);
}

public static IEnumerable<object[]> GetAdditionalConstructedTypeData()
{
yield return new object[] { typeof(Dictionary<List<int[]>[,], List<int?[][][,]>>[]), 16 };

// "Dictionary<List<int[]>[,], List<int?[][][,]>>[]" breaks down to complexity 16 like so:
//
// 01: Dictionary<List<int[]>[,], List<int?[][][,]>>[]
// 02: `- Dictionary<List<int[]>[,], List<int?[][][,]>>
// 03: +- Dictionary`2
// 04: +- List<int[]>[,]
// 05: | `- List<int[]>
// 06: | +- List`1
// 07: | `- int[]
// 08: | `- int
// 09: `- List<int?[][][,]>
// 10: +- List`1
// 11: `- int?[][][,]
// 12: `- int?[][]
// 13: `- int?[]
// 14: `- int?
// 15: +- Nullable`1
// 16: `- int

yield return new object[] { typeof(int[]).MakePointerType().MakeByRefType(), 4 }; // int[]*&
yield return new object[] { typeof(long).MakeArrayType(31), 2 }; // long[,,,,,,,...]
yield return new object[] { typeof(long).Assembly.GetType("System.Int64[*]"), 2 }; // long[*]
}

[Theory]
[InlineData(typeof(TypeName), 1)]
[InlineData(typeof(TypeNameParserTests), 1)]
[InlineData(typeof(object), 1)]
[InlineData(typeof(Assert), 1)] // xunit
[InlineData(typeof(int[]), 2)]
[InlineData(typeof(int[,][]), 3)]
[InlineData(typeof(Nullable<>), 1)] // open generic type treated as elemental
[MemberData(nameof(GetAdditionalConstructedTypeData))]
public void TotalComplexityReturnsExpectedValue(Type type, int expectedComplexity)
{
TypeName parsed = TypeName.Parse(type.AssemblyQualifiedName.AsSpan());

Assert.Equal(expectedComplexity, parsed.TotalComplexity);
}

[Theory]
[InlineData(typeof(int))]
[InlineData(typeof(int?))]
Expand Down

0 comments on commit ee43e71

Please sign in to comment.