Skip to content
69 changes: 69 additions & 0 deletions ICSharpCode.Decompiler.Tests/Output/EscapeIdentifierTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) 2026 Christoph Wille
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using ICSharpCode.Decompiler.CSharp.OutputVisitor;

using NUnit.Framework;

namespace ICSharpCode.Decompiler.Tests.Output
{
[TestFixture]
public class EscapeIdentifierTests
{
[Test]
public void PlainIdentifierIsReturnedAsTheSameInstance()
{
// The overwhelmingly common case must not allocate at all.
string identifier = "MyIdentifier_42";
Assert.That(TextWriterTokenWriter.EscapeIdentifier(identifier), Is.SameAs(identifier));
}

[Test]
public void EmptyAndNullAreReturnedUnchanged()
{
Assert.That(TextWriterTokenWriter.EscapeIdentifier(""), Is.EqualTo(""));
Assert.That(TextWriterTokenWriter.EscapeIdentifier(null), Is.Null);
}

[Test]
public void ControlCharIsEscaped()
{
Assert.That(TextWriterTokenWriter.EscapeIdentifier("a\u0001b"), Is.EqualTo(@"a\u0001b"));
}

[Test]
public void BackslashIsEscaped()
{
Assert.That(TextWriterTokenWriter.EscapeIdentifier("a\\b"), Is.EqualTo(@"a\u005cb"));
}

[Test]
public void PrintableSurrogatePairPassesThroughUnchanged()
{
// U+1D49C (MATHEMATICAL SCRIPT CAPITAL A) is a letter, i.e. printable.
Assert.That(TextWriterTokenWriter.EscapeIdentifier("a\U0001D49Cb"), Is.EqualTo("a\U0001D49Cb"));
}

[Test]
public void NonPrintableSurrogatePairIsEscapedAsUtf32()
{
// U+1D173 (MUSICAL SYMBOL BEGIN BEAM) is a format char, i.e. non-printable.
Assert.That(TextWriterTokenWriter.EscapeIdentifier("a\U0001D173b"), Is.EqualTo(@"a\U0001d173b"));
}
}
}
78 changes: 78 additions & 0 deletions ICSharpCode.Decompiler.Tests/TypeSystem/ReflectionHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,84 @@ public void ParseInvalidReflectionName12()
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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,14 +514,17 @@ public static string EscapeIdentifier(string identifier)
{
if (string.IsNullOrEmpty(identifier))
return identifier;
StringBuilder sb = new StringBuilder();
if (!NeedsEscaping(identifier))
return identifier;
StringBuilder sb = new StringBuilder(identifier.Length);
for (int i = 0; i < identifier.Length; i++)
{
if (IsPrintableIdentifierChar(identifier, i))
{
if (char.IsSurrogatePair(identifier, i))
{
sb.Append(identifier.Substring(i, 2));
sb.Append(identifier[i]);
sb.Append(identifier[i + 1]);
i++;
}
else
Expand All @@ -545,11 +548,25 @@ public static string EscapeIdentifier(string identifier)
return sb.ToString();
}

static bool NeedsEscaping(string identifier)
{
for (int i = 0; i < identifier.Length; i++)
{
if (!IsPrintableIdentifierChar(identifier, i))
return true;
if (char.IsSurrogatePair(identifier, i))
i++;
}
return false;
}

public static bool ContainsNonPrintableIdentifierChar(string identifier)
{
if (string.IsNullOrEmpty(identifier))
return false;
return !string.IsNullOrEmpty(identifier) && ContainsNonPrintableIdentifierChar(identifier.AsSpan());
}

public static bool ContainsNonPrintableIdentifierChar(ReadOnlySpan<char> identifier)
{
for (int i = 0; i < identifier.Length; i++)
{
if (char.IsWhiteSpace(identifier[i]))
Expand All @@ -562,6 +579,11 @@ public static bool ContainsNonPrintableIdentifierChar(string identifier)
}

static bool IsPrintableIdentifierChar(string identifier, int index)
{
return IsPrintableIdentifierChar(identifier.AsSpan(), index);
}

static bool IsPrintableIdentifierChar(ReadOnlySpan<char> identifier, int index)
{
switch (identifier[index])
{
Expand All @@ -573,7 +595,18 @@ static bool IsPrintableIdentifierChar(string identifier, int index)
case '^':
return true;
}
switch (char.GetUnicodeCategory(identifier, index))
UnicodeCategory category;
if (index + 1 < identifier.Length && char.IsSurrogatePair(identifier[index], identifier[index + 1]))
{
// netstandard2.0 has no code-point-based GetUnicodeCategory, so the rare
// astral-plane case pays for a two-char string to categorize the pair.
category = char.GetUnicodeCategory(new string(new[] { identifier[index], identifier[index + 1] }), 0);
}
else
{
category = char.GetUnicodeCategory(identifier[index]);
}
switch (category)
{
case UnicodeCategory.NonSpacingMark:
case UnicodeCategory.SpacingCombiningMark:
Expand Down
17 changes: 12 additions & 5 deletions ICSharpCode.Decompiler/Disassembler/MethodBodyDisassembler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -593,19 +593,26 @@ void WriteRVA(BlobReader blob, int offset, ILOpCode opCode)
}
}

// The shortcut-form opcodes cover exactly the indices 0-3, so the digit text and the
// local-reference keys can come from fixed tables instead of being allocated per
// rendered instruction.
static readonly string[] shortcutIndexes = { "0", "1", "2", "3" };
static readonly string[] shortcutParamReferences = { "param_0", "param_1", "param_2", "param_3" };
static readonly string[] shortcutLocReferences = { "loc_0", "loc_1", "loc_2", "loc_3" };

private void WriteOpCode(ILOpCode opCode)
{
var opCodeInfo = new OpCodeInfo(opCode, opCode.GetDisplayName());
string index;
int index;
switch (opCode)
{
case ILOpCode.Ldarg_0:
case ILOpCode.Ldarg_1:
case ILOpCode.Ldarg_2:
case ILOpCode.Ldarg_3:
output.WriteReference(opCodeInfo, omitSuffix: true);
index = opCodeInfo.Name.Substring(6);
output.WriteLocalReference(index, "param_" + index);
index = opCode - ILOpCode.Ldarg_0;
output.WriteLocalReference(shortcutIndexes[index], shortcutParamReferences[index]);
break;
case ILOpCode.Ldloc_0:
case ILOpCode.Ldloc_1:
Expand All @@ -616,8 +623,8 @@ private void WriteOpCode(ILOpCode opCode)
case ILOpCode.Stloc_2:
case ILOpCode.Stloc_3:
output.WriteReference(opCodeInfo, omitSuffix: true);
index = opCodeInfo.Name.Substring(6);
output.WriteLocalReference(index, "loc_" + index);
index = opCode <= ILOpCode.Ldloc_3 ? opCode - ILOpCode.Ldloc_0 : opCode - ILOpCode.Stloc_0;
output.WriteLocalReference(shortcutIndexes[index], shortcutLocReferences[index]);
break;
default:
output.WriteReference(opCodeInfo);
Expand Down
Loading
Loading