Browse Source

Fuse the reflection-name and arity parse to cut each string once

SplitTypeParameterCountFromReflectionName allocated the digits after
the backtick only to feed int.TryParse and discard them, and the
TopLevelTypeName constructor cut the name part twice for generic
types (once at the dot, again at the backtick), dropping the first
cut. Both run for every generic reflection name parsed, e.g. for
typeof-valued attribute arguments and string-switch metadata. The
arity is now parsed in place with a digit loop (netstandard2.0 has
no span int.TryParse) and each final string is cut exactly once.

The digit loop only accepts plain ASCII digits, so suffixes like
`+1 that int.TryParse tolerated are now rejected; such names are not
legal reflection names. New unit tests pin the parse edge cases.

Assisted-by: Claude:claude-fable-5:Claude Code
pull/3963/head
Christoph Wille 1 month ago
parent
commit
55fbd563e2
  1. 78
      ICSharpCode.Decompiler.Tests/TypeSystem/ReflectionHelperTests.cs
  2. 37
      ICSharpCode.Decompiler/TypeSystem/ReflectionHelper.cs
  3. 12
      ICSharpCode.Decompiler/TypeSystem/TopLevelTypeName.cs

78
ICSharpCode.Decompiler.Tests/TypeSystem/ReflectionHelperTests.cs

@ -266,6 +266,84 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem @@ -266,6 +266,84 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem
Assert.Throws<ReflectionNameParseException>(() => ReflectionHelper.ParseReflectionName("System.Action`1[[System.Int32]a]", context));
}
[Test]
public void SplitTypeParameterCountFromName()
{
Assert.That(ReflectionHelper.SplitTypeParameterCountFromReflectionName("List`1", out int tpc), Is.EqualTo("List"));
Assert.That(tpc, Is.EqualTo(1));
Assert.That(ReflectionHelper.SplitTypeParameterCountFromReflectionName("Dictionary`2", out tpc), Is.EqualTo("Dictionary"));
Assert.That(tpc, Is.EqualTo(2));
Assert.That(ReflectionHelper.SplitTypeParameterCountFromReflectionName("Foo`12", out tpc), Is.EqualTo("Foo"));
Assert.That(tpc, Is.EqualTo(12));
}
[Test]
public void SplitTypeParameterCountWithoutBacktick()
{
Assert.That(ReflectionHelper.SplitTypeParameterCountFromReflectionName("String", out int tpc), Is.EqualTo("String"));
Assert.That(tpc, Is.EqualTo(0));
}
[Test]
public void SplitTypeParameterCountUsesTheLastBacktick()
{
Assert.That(ReflectionHelper.SplitTypeParameterCountFromReflectionName("Outer`1+Inner`2", out int tpc), Is.EqualTo("Outer`1+Inner"));
Assert.That(tpc, Is.EqualTo(2));
}
[Test]
public void SplitTypeParameterCountKeepsNameWhenSuffixIsNotAPlainNumber()
{
Assert.That(ReflectionHelper.SplitTypeParameterCountFromReflectionName("Foo`", out int tpc), Is.EqualTo("Foo`"));
Assert.That(tpc, Is.EqualTo(0));
Assert.That(ReflectionHelper.SplitTypeParameterCountFromReflectionName("Foo`x", out tpc), Is.EqualTo("Foo`x"));
Assert.That(tpc, Is.EqualTo(0));
// Only plain digits form an arity: a signed suffix is not a legal reflection name.
Assert.That(ReflectionHelper.SplitTypeParameterCountFromReflectionName("Foo`+1", out tpc), Is.EqualTo("Foo`+1"));
Assert.That(tpc, Is.EqualTo(0));
// An arity beyond int.MaxValue is rejected, not truncated.
Assert.That(ReflectionHelper.SplitTypeParameterCountFromReflectionName("Foo`2147483648", out tpc), Is.EqualTo("Foo`2147483648"));
Assert.That(tpc, Is.EqualTo(0));
Assert.That(ReflectionHelper.SplitTypeParameterCountFromReflectionName("Foo`99999999999999999999", out tpc), Is.EqualTo("Foo`99999999999999999999"));
Assert.That(tpc, Is.EqualTo(0));
}
[Test]
public void TopLevelTypeNameParsesNamespaceNameAndArity()
{
var t = new TopLevelTypeName("System.Collections.Generic.List`1");
Assert.That(t.Namespace, Is.EqualTo("System.Collections.Generic"));
Assert.That(t.Name, Is.EqualTo("List"));
Assert.That(t.TypeParameterCount, Is.EqualTo(1));
}
[Test]
public void TopLevelTypeNameWithoutNamespace()
{
var t = new TopLevelTypeName("List`1");
Assert.That(t.Namespace, Is.EqualTo(string.Empty));
Assert.That(t.Name, Is.EqualTo("List"));
Assert.That(t.TypeParameterCount, Is.EqualTo(1));
}
[Test]
public void TopLevelTypeNameWithoutArity()
{
var t = new TopLevelTypeName("System.String");
Assert.That(t.Namespace, Is.EqualTo("System"));
Assert.That(t.Name, Is.EqualTo("String"));
Assert.That(t.TypeParameterCount, Is.EqualTo(0));
}
[Test]
public void TopLevelTypeNameIgnoresBacktickInsideTheNamespace()
{
var t = new TopLevelTypeName("A`1.B");
Assert.That(t.Namespace, Is.EqualTo("A`1"));
Assert.That(t.Name, Is.EqualTo("B"));
Assert.That(t.TypeParameterCount, Is.EqualTo(0));
}
[Test]
public void ParseInvalidReflectionName13()
{

37
ICSharpCode.Decompiler/TypeSystem/ReflectionHelper.cs

@ -83,19 +83,38 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -83,19 +83,38 @@ namespace ICSharpCode.Decompiler.TypeSystem
public static string SplitTypeParameterCountFromReflectionName(string reflectionName, out int typeParameterCount)
{
int pos = reflectionName.LastIndexOf('`');
if (pos < 0)
if (pos >= 0 && TryParseTypeParameterCount(reflectionName, pos + 1, out typeParameterCount))
{
typeParameterCount = 0;
return reflectionName;
return reflectionName.Substring(0, pos);
}
else
typeParameterCount = 0;
return reflectionName;
}
/// <summary>
/// Parses a type parameter count that starts at <paramref name="start"/> and extends to
/// the end of <paramref name="reflectionName"/>. Only plain ASCII digits are accepted
/// (no sign or whitespace), because that is all a legal reflection name can contain.
/// netstandard2.0 has no span-based int.TryParse, so the digits are accumulated manually
/// to avoid allocating a throwaway substring.
/// </summary>
internal static bool TryParseTypeParameterCount(string reflectionName, int start, out int typeParameterCount)
{
typeParameterCount = 0;
if (start >= reflectionName.Length)
return false;
long value = 0;
for (int i = start; i < reflectionName.Length; i++)
{
string typeCount = reflectionName.Substring(pos + 1);
if (int.TryParse(typeCount, out typeParameterCount))
return reflectionName.Substring(0, pos);
else
return reflectionName;
char c = reflectionName[i];
if (c < '0' || c > '9')
return false;
value = value * 10 + (c - '0');
if (value > int.MaxValue)
return false;
}
typeParameterCount = (int)value;
return true;
}
#endregion

12
ICSharpCode.Decompiler/TypeSystem/TopLevelTypeName.cs

@ -46,18 +46,20 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -46,18 +46,20 @@ namespace ICSharpCode.Decompiler.TypeSystem
public TopLevelTypeName(string reflectionName)
{
// Locate both separators up front so that namespaceName and name are each cut
// exactly once, without an intermediate string still carrying the arity suffix.
int pos = reflectionName.LastIndexOf('.');
if (pos < 0)
int tick = reflectionName.LastIndexOf('`');
if (tick > pos && ReflectionHelper.TryParseTypeParameterCount(reflectionName, tick + 1, out typeParameterCount))
{
namespaceName = string.Empty;
name = reflectionName;
name = reflectionName.Substring(pos + 1, tick - pos - 1);
}
else
{
namespaceName = reflectionName.Substring(0, pos);
typeParameterCount = 0;
name = reflectionName.Substring(pos + 1);
}
name = ReflectionHelper.SplitTypeParameterCountFromReflectionName(name, out typeParameterCount);
namespaceName = pos < 0 ? string.Empty : reflectionName.Substring(0, pos);
}
public string Namespace {

Loading…
Cancel
Save