From 55fbd563e20feb39a50b4fde0887e2ea9a48b904 Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Fri, 7 Aug 2026 14:28:42 +0200 Subject: [PATCH] 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 --- .../TypeSystem/ReflectionHelperTests.cs | 78 +++++++++++++++++++ .../TypeSystem/ReflectionHelper.cs | 37 ++++++--- .../TypeSystem/TopLevelTypeName.cs | 12 +-- 3 files changed, 113 insertions(+), 14 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/ReflectionHelperTests.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/ReflectionHelperTests.cs index 66b24d03e..fa5ec206b 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/ReflectionHelperTests.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/ReflectionHelperTests.cs @@ -266,6 +266,84 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem Assert.Throws(() => 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() { diff --git a/ICSharpCode.Decompiler/TypeSystem/ReflectionHelper.cs b/ICSharpCode.Decompiler/TypeSystem/ReflectionHelper.cs index edc2dfc30..7530bb5cd 100644 --- a/ICSharpCode.Decompiler/TypeSystem/ReflectionHelper.cs +++ b/ICSharpCode.Decompiler/TypeSystem/ReflectionHelper.cs @@ -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; + } + + /// + /// Parses a type parameter count that starts at and extends to + /// the end of . 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. + /// + 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 diff --git a/ICSharpCode.Decompiler/TypeSystem/TopLevelTypeName.cs b/ICSharpCode.Decompiler/TypeSystem/TopLevelTypeName.cs index e975d62ac..c6f77f163 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TopLevelTypeName.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TopLevelTypeName.cs @@ -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 {