mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
Merge pull request #3963 from icsharpcode/substring-optimizations
Substring optimizations: eliminate hot-path allocations (audit top 8)
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -240,8 +240,8 @@ namespace ICSharpCode.Decompiler.CSharp
|
||||
/// Gets or sets whether the pipeline records its steps into <see cref="Stepper"/>, so that one
|
||||
/// step tree spans all of it: the IL transforms of every member, the ILAst-to-C# conversion,
|
||||
/// and the C# AST transforms.
|
||||
/// Off by default, and off means the whole pipeline records nothing: every phase keeps the
|
||||
/// stepper its context creates for itself, which nothing reads. Turning it on is what makes the
|
||||
/// Off by default, and off means the whole pipeline records nothing: contexts do not get a
|
||||
/// stepper unless this is enabled. Turning it on is what makes the
|
||||
/// steps <i>retained</i>, and each retained step pins the ILAst it captured, including the
|
||||
/// instructions its transform removed. That is affordable for the single type a step view
|
||||
/// displays and not for a whole-module decompile, so only callers that show the steps opt in.
|
||||
@@ -910,8 +910,7 @@ namespace ICSharpCode.Decompiler.CSharp
|
||||
var context = new TransformContext(typeSystem, decompileRun, decompilationContext, typeSystemAstBuilder);
|
||||
// Off means off for the whole pipeline: leaving the AST half recording would give the same
|
||||
// pipeline two numbering bases, and an index recorded under one of them means a different
|
||||
// step under the other. Without this the context keeps a stepper of its own, which nothing
|
||||
// reads.
|
||||
// step under the other. Without this the context does not record steps.
|
||||
if (RecordSteps)
|
||||
context.Stepper = Stepper;
|
||||
// The tree handed to the pipeline must already be well-formed; check it once up front so a
|
||||
|
||||
@@ -3784,7 +3784,7 @@ namespace ICSharpCode.Decompiler.CSharp
|
||||
{
|
||||
var property = (IProperty)lastElement.Member;
|
||||
Debug.Assert(property.Parameters.Count == indices);
|
||||
Debug.Assert(property.Setter != null, $"Indexer property {property} has no setter");
|
||||
DecompilerAssert.That(property.Setter != null, $"Indexer property {property} has no setter");
|
||||
elementsStack.Peek().Add(
|
||||
new CallBuilder(this, typeSystem, settings)
|
||||
.BuildDictionaryInitializerExpression(lastElement.OpCode, property.Setter, initObjRR, GetIndices(lastElement.Indices, indexVariables).ToList(), info.Values!.Single())
|
||||
|
||||
@@ -514,14 +514,17 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
|
||||
{
|
||||
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
|
||||
@@ -545,11 +548,25 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
|
||||
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]))
|
||||
@@ -562,6 +579,11 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
|
||||
}
|
||||
|
||||
static bool IsPrintableIdentifierChar(string identifier, int index)
|
||||
{
|
||||
return IsPrintableIdentifierChar(identifier.AsSpan(), index);
|
||||
}
|
||||
|
||||
static bool IsPrintableIdentifierChar(ReadOnlySpan<char> identifier, int index)
|
||||
{
|
||||
switch (identifier[index])
|
||||
{
|
||||
@@ -573,7 +595,18 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
|
||||
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:
|
||||
|
||||
@@ -35,6 +35,7 @@ using System.Linq;
|
||||
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
|
||||
using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching;
|
||||
using ICSharpCode.Decompiler.TypeSystem;
|
||||
using ICSharpCode.Decompiler.Util;
|
||||
|
||||
namespace ICSharpCode.Decompiler.CSharp.Syntax
|
||||
{
|
||||
@@ -577,7 +578,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
|
||||
{
|
||||
// A single slot reads null only when empty; that is valid only if the slot is optional.
|
||||
// (Collection slots never yield a null index, so this branch is always a single slot.)
|
||||
Debug.Assert(slot.IsOptional, $"required slot '{slot.Name}' on {GetType().Name} must not be empty");
|
||||
DecompilerAssert.That(slot.IsOptional, $"required slot '{slot.Name}' on {GetType().Name} must not be empty");
|
||||
continue;
|
||||
}
|
||||
Debug.Assert(child.parent == this, "child's Parent must point back to this node");
|
||||
|
||||
@@ -33,6 +33,7 @@ using ICSharpCode.Decompiler.IL;
|
||||
using ICSharpCode.Decompiler.Semantics;
|
||||
using ICSharpCode.Decompiler.TypeSystem;
|
||||
using ICSharpCode.Decompiler.TypeSystem.Implementation;
|
||||
using ICSharpCode.Decompiler.Util;
|
||||
|
||||
namespace ICSharpCode.Decompiler.CSharp.Transforms
|
||||
{
|
||||
@@ -61,7 +62,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
|
||||
.ThenByDescending(n => n);
|
||||
foreach (string ns in sortedImports)
|
||||
{
|
||||
Debug.Assert(context.RequiredNamespacesSuperset.Contains(ns), $"Should not insert using declaration for namespace that is missing from the superset: {ns}");
|
||||
DecompilerAssert.That(context.RequiredNamespacesSuperset.Contains(ns), $"Should not insert using declaration for namespace that is missing from the superset: {ns}");
|
||||
// we go backwards (OrderByDescending) through the list of namespaces because we insert them backwards
|
||||
// (always inserting at the start of the list)
|
||||
string[] parts = ns.Split('.');
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
|
||||
public readonly TypeSystemAstBuilder TypeSystemAstBuilder;
|
||||
public readonly DecompilerSettings Settings;
|
||||
internal readonly DecompileRun DecompileRun;
|
||||
public Stepper Stepper { get; set; }
|
||||
public Stepper? Stepper { get; set; }
|
||||
|
||||
readonly ITypeResolveContext decompilationContext;
|
||||
|
||||
@@ -71,7 +71,6 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
|
||||
this.TypeSystemAstBuilder = typeSystemAstBuilder;
|
||||
this.CancellationToken = decompileRun.CancellationToken;
|
||||
this.Settings = decompileRun.Settings;
|
||||
this.Stepper = new Stepper();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -82,20 +81,20 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
|
||||
[DebuggerStepThrough]
|
||||
internal void Step(string description, AstNode? near = null)
|
||||
{
|
||||
Stepper.Step(description, CreateNodeInfo(near));
|
||||
Stepper?.Step(description, CreateNodeInfo(near));
|
||||
}
|
||||
|
||||
[Conditional("STEP")]
|
||||
[DebuggerStepThrough]
|
||||
internal void StepStartGroup(string description, AstNode? near = null)
|
||||
{
|
||||
Stepper.StartGroup(description, CreateNodeInfo(near));
|
||||
Stepper?.StartGroup(description, CreateNodeInfo(near));
|
||||
}
|
||||
|
||||
[Conditional("STEP")]
|
||||
internal void StepEndGroup(bool keepIfEmpty = false)
|
||||
{
|
||||
Stepper.EndGroup(keepIfEmpty);
|
||||
Stepper?.EndGroup(keepIfEmpty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -106,7 +105,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
|
||||
[Conditional("STEP")]
|
||||
internal void EndStep(AstNode? modifiedNode)
|
||||
{
|
||||
if (Stepper.LastStep != null && modifiedNode != null)
|
||||
if (Stepper?.LastStep != null && modifiedNode != null)
|
||||
{
|
||||
var marker = new DebugStepMarker();
|
||||
modifiedNode.AddAnnotation(marker);
|
||||
|
||||
@@ -605,10 +605,17 @@ namespace ICSharpCode.Decompiler.Disassembler
|
||||
}
|
||||
}
|
||||
|
||||
// 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:
|
||||
@@ -616,8 +623,8 @@ namespace ICSharpCode.Decompiler.Disassembler
|
||||
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:
|
||||
@@ -628,8 +635,8 @@ namespace ICSharpCode.Decompiler.Disassembler
|
||||
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);
|
||||
|
||||
@@ -710,7 +710,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
|
||||
internal static bool IsValidName(string varName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(varName))
|
||||
return varName != null && IsValidName(varName.AsSpan());
|
||||
}
|
||||
|
||||
static bool IsValidName(ReadOnlySpan<char> varName)
|
||||
{
|
||||
if (varName.IsEmpty || varName.IsWhiteSpace())
|
||||
return false;
|
||||
if (!(char.IsLetter(varName[0]) || varName[0] == '_'))
|
||||
return false;
|
||||
@@ -745,12 +750,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
if (m.Name.StartsWith("get_", StringComparison.OrdinalIgnoreCase) && m.Parameters.Count == 0)
|
||||
{
|
||||
// use name from properties, but not from indexers
|
||||
return CleanUpVariableName(m.Name.Substring(4));
|
||||
return CleanUpVariableName(m.Name.AsSpan(4));
|
||||
}
|
||||
else if (m.Name.StartsWith("Get", StringComparison.OrdinalIgnoreCase) && m.Name.Length >= 4 && char.IsUpper(m.Name[3]))
|
||||
{
|
||||
// use name from Get-methods
|
||||
return CleanUpVariableName(m.Name.Substring(3));
|
||||
return CleanUpVariableName(m.Name.AsSpan(3));
|
||||
}
|
||||
break;
|
||||
case DynamicInvokeMemberInstruction dynInvokeMember:
|
||||
@@ -758,7 +763,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
&& dynInvokeMember.Name.Length >= 4 && char.IsUpper(dynInvokeMember.Name[3]))
|
||||
{
|
||||
// use name from Get-methods
|
||||
return CleanUpVariableName(dynInvokeMember.Name.Substring(3));
|
||||
return CleanUpVariableName(dynInvokeMember.Name.AsSpan(3));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -787,11 +792,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
// argument might be value of a setter
|
||||
if (m.Name.StartsWith("set_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return CleanUpVariableName(m.Name.Substring(4));
|
||||
return CleanUpVariableName(m.Name.AsSpan(4));
|
||||
}
|
||||
else if (m.Name.StartsWith("Set", StringComparison.OrdinalIgnoreCase) && m.Name.Length >= 4 && char.IsUpper(m.Name[3]))
|
||||
{
|
||||
return CleanUpVariableName(m.Name.Substring(3));
|
||||
return CleanUpVariableName(m.Name.AsSpan(3));
|
||||
}
|
||||
}
|
||||
var p = call.GetParameter(i);
|
||||
@@ -857,9 +862,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
_ => type.Name
|
||||
};
|
||||
// remove the 'I' for interfaces
|
||||
if (name.Length >= 3 && name[0] == 'I' && char.IsUpper(name[1]) && char.IsLower(name[2]))
|
||||
name = name.Substring(1);
|
||||
name = CleanUpVariableName(name) ?? "obj";
|
||||
ReadOnlySpan<char> nameSpan = name.AsSpan();
|
||||
if (nameSpan.Length >= 3 && nameSpan[0] == 'I' && char.IsUpper(nameSpan[1]) && char.IsLower(nameSpan[2]))
|
||||
nameSpan = nameSpan.Slice(1);
|
||||
name = CleanUpVariableName(nameSpan) ?? "obj";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
@@ -887,8 +893,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
pos--;
|
||||
if (pos < name.Length)
|
||||
{
|
||||
if (int.TryParse(name.Substring(pos), out number))
|
||||
// The loop above guarantees name[pos..] is all ASCII digits;
|
||||
// accumulate the value inline, giving up on int overflow.
|
||||
long value = 0;
|
||||
for (int i = pos; i < name.Length; i++)
|
||||
{
|
||||
value = value * 10 + (name[i] - '0');
|
||||
if (value > int.MaxValue)
|
||||
break;
|
||||
}
|
||||
if (value <= int.MaxValue)
|
||||
{
|
||||
number = (int)value;
|
||||
return name.Substring(0, pos);
|
||||
}
|
||||
}
|
||||
@@ -897,17 +913,22 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
}
|
||||
|
||||
static string CleanUpVariableName(string name)
|
||||
{
|
||||
return CleanUpVariableName(name.AsSpan());
|
||||
}
|
||||
|
||||
static string CleanUpVariableName(ReadOnlySpan<char> name)
|
||||
{
|
||||
// remove the backtick (generics)
|
||||
int pos = name.IndexOf('`');
|
||||
if (pos >= 0)
|
||||
name = name.Substring(0, pos);
|
||||
name = name.Slice(0, pos);
|
||||
|
||||
// remove field prefix:
|
||||
if (name.Length > 2 && name.StartsWith("m_", StringComparison.Ordinal))
|
||||
name = name.Substring(2);
|
||||
if (name.Length > 2 && name.StartsWith("m_".AsSpan(), StringComparison.Ordinal))
|
||||
name = name.Slice(2);
|
||||
else if (name.Length > 1 && name[0] == '_' && (char.IsLetter(name[1]) || name[1] == '_'))
|
||||
name = name.Substring(1);
|
||||
name = name.Slice(1);
|
||||
|
||||
if (TextWriterTokenWriter.ContainsNonPrintableIdentifierChar(name))
|
||||
{
|
||||
@@ -922,7 +943,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
// separates the parts of its generated names with '$'.
|
||||
return null;
|
||||
}
|
||||
string lowerCaseName = char.ToLower(name[0]) + name.Substring(1);
|
||||
// lowercase the first char, materializing the result in a single allocation
|
||||
// (netstandard2.0 has no string(ReadOnlySpan<char>) constructor)
|
||||
char[] chars = new char[name.Length];
|
||||
chars[0] = char.ToLower(name[0]);
|
||||
name.Slice(1).CopyTo(chars.AsSpan(1));
|
||||
string lowerCaseName = new string(chars);
|
||||
if (CSharp.OutputVisitor.CSharpOutputVisitor.IsKeyword(lowerCaseName))
|
||||
return null;
|
||||
return lowerCaseName;
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
continue;
|
||||
if (!callsites.TryGetValue(cacheField, out var callsite))
|
||||
continue;
|
||||
context.Stepper.Step("Transform callsite for " + callsite.MemberName);
|
||||
context.Step("Transform callsite for " + callsite.MemberName, invokeCall);
|
||||
var deadArguments = new List<ILInstruction>();
|
||||
ILInstruction replacement = MakeDynamicInstruction(callsite, invokeCall, deadArguments);
|
||||
if (replacement == null)
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
public IDebugInfoProvider? DebugInfo { get; }
|
||||
public DecompilerSettings Settings { get; }
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
public Stepper Stepper { get; set; }
|
||||
public Stepper? Stepper { get; set; }
|
||||
public Metadata.MetadataFile PEFile => TypeSystem.MainModule.MetadataFile;
|
||||
|
||||
internal DecompileRun? DecompileRun { get; set; }
|
||||
@@ -73,7 +73,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
this.TypeSystem = typeSystem ?? throw new ArgumentNullException(nameof(typeSystem));
|
||||
this.Settings = settings ?? new DecompilerSettings();
|
||||
this.DebugInfo = debugInfo;
|
||||
Stepper = new Stepper();
|
||||
}
|
||||
|
||||
public ILTransformContext(ILTransformContext context, ILFunction? function = null)
|
||||
@@ -107,20 +106,20 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
[DebuggerStepThrough]
|
||||
internal void Step(string description, ILInstruction? near)
|
||||
{
|
||||
Stepper.Step(description, CreateNodeInfo(near));
|
||||
Stepper?.Step(description, CreateNodeInfo(near));
|
||||
}
|
||||
|
||||
[Conditional("STEP")]
|
||||
[DebuggerStepThrough]
|
||||
internal void StepStartGroup(string description, ILInstruction? near = null)
|
||||
{
|
||||
Stepper.StartGroup(description, CreateNodeInfo(near));
|
||||
Stepper?.StartGroup(description, CreateNodeInfo(near));
|
||||
}
|
||||
|
||||
[Conditional("STEP")]
|
||||
internal void StepEndGroup(bool keepIfEmpty = false)
|
||||
{
|
||||
Stepper.EndGroup(keepIfEmpty);
|
||||
Stepper?.EndGroup(keepIfEmpty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -134,7 +133,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|
||||
[Conditional("STEP")]
|
||||
internal void EndStep(ILInstruction? modifiedNode)
|
||||
{
|
||||
if (Stepper.LastStep is { } step && modifiedNode != null)
|
||||
if (Stepper?.LastStep is { } step && modifiedNode != null)
|
||||
{
|
||||
step.ModifiedNode = modifiedNode;
|
||||
step.RecordModifiedNode(modifiedNode, insertFirst: true);
|
||||
|
||||
@@ -102,12 +102,13 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
|
||||
// with MethodAttributes.SpecialName or MethodAttributes.RTSpecialName
|
||||
string name = this.Name;
|
||||
int index = name.LastIndexOf('.');
|
||||
if (index > 0)
|
||||
// Test the op_ prefix on a slice first: this branch runs for every static
|
||||
// non-generic method, and only operator names warrant the substring.
|
||||
if (index > 0 && name.AsSpan(index + 1).StartsWith("op_".AsSpan(), StringComparison.Ordinal))
|
||||
{
|
||||
name = name.Substring(index + 1);
|
||||
|
||||
if (name.StartsWith("op_", StringComparison.Ordinal)
|
||||
&& CSharp.Syntax.OperatorDeclaration.GetOperatorType(name) != null)
|
||||
if (CSharp.Syntax.OperatorDeclaration.GetOperatorType(name) != null)
|
||||
{
|
||||
this.symbolKind = SymbolKind.Operator;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2026 Siegfried Pammer
|
||||
//
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace ICSharpCode.Decompiler.Util
|
||||
{
|
||||
static class DecompilerAssert
|
||||
{
|
||||
[Conditional("DEBUG")]
|
||||
public static void That([DoesNotReturnIf(false)] bool condition)
|
||||
{
|
||||
#if DEBUG
|
||||
if (!condition)
|
||||
Debug.Fail("Assertion failed.");
|
||||
#endif
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
public static void That([DoesNotReturnIf(false)] bool condition,
|
||||
[InterpolatedStringHandlerArgument(nameof(condition))] ref AssertInterpolatedStringHandler message)
|
||||
{
|
||||
#if DEBUG
|
||||
if (!condition)
|
||||
Debug.Fail(message.ToString());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
[InterpolatedStringHandler]
|
||||
ref struct AssertInterpolatedStringHandler
|
||||
{
|
||||
readonly bool enabled;
|
||||
StringBuilder? builder;
|
||||
|
||||
public AssertInterpolatedStringHandler(int literalLength, int formattedCount, bool condition, out bool shouldAppend)
|
||||
{
|
||||
enabled = !condition;
|
||||
shouldAppend = enabled;
|
||||
builder = enabled ? new StringBuilder(literalLength) : null;
|
||||
}
|
||||
|
||||
public void AppendLiteral(string value)
|
||||
{
|
||||
if (enabled)
|
||||
builder!.Append(value);
|
||||
}
|
||||
|
||||
public void AppendFormatted<T>(T value)
|
||||
{
|
||||
if (enabled)
|
||||
builder!.Append(value);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return builder?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
|
||||
sealed class InterpolatedStringHandlerAttribute : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
|
||||
sealed class InterpolatedStringHandlerArgumentAttribute : Attribute
|
||||
{
|
||||
public InterpolatedStringHandlerArgumentAttribute(string argument)
|
||||
{
|
||||
Arguments = new[] { argument };
|
||||
}
|
||||
|
||||
public InterpolatedStringHandlerArgumentAttribute(params string[] arguments)
|
||||
{
|
||||
Arguments = arguments;
|
||||
}
|
||||
|
||||
public string[] Arguments { get; }
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
|
||||
@@ -69,12 +70,40 @@ namespace ICSharpCode.ILSpyX.Search
|
||||
|
||||
public abstract class AbstractSearchStrategy
|
||||
{
|
||||
enum TermOperator
|
||||
{
|
||||
Contains,
|
||||
NotContains,
|
||||
Exact,
|
||||
Fuzzy
|
||||
}
|
||||
|
||||
readonly struct PreparedTerm
|
||||
{
|
||||
// For Exact this is the unstripped term (including the '=' prefix), because the
|
||||
// comparison below works with an offset of 1 and the full term length; for Fuzzy
|
||||
// it is the stripped term lowered once with ToLowerInvariant; for the others it
|
||||
// is the term with any '+'/'-' prefix stripped.
|
||||
public readonly string Text;
|
||||
public readonly TermOperator Operator;
|
||||
|
||||
public PreparedTerm(TermOperator op, string text)
|
||||
{
|
||||
this.Operator = op;
|
||||
this.Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly string[] searchTerm;
|
||||
protected readonly Regex? regex;
|
||||
protected readonly bool fullNameSearch;
|
||||
protected readonly bool omitGenerics;
|
||||
protected readonly SearchRequest searchRequest;
|
||||
private readonly IProducerConsumerCollection<SearchResult> resultQueue;
|
||||
// The search terms are invariant for the lifetime of a strategy (each keystroke
|
||||
// creates a new request + strategy), so prefix stripping and lowercasing are done
|
||||
// once here instead of per candidate name in IsMatch.
|
||||
private readonly PreparedTerm[] preparedTerms;
|
||||
|
||||
protected AbstractSearchStrategy(SearchRequest request, IProducerConsumerCollection<SearchResult> resultQueue)
|
||||
{
|
||||
@@ -84,6 +113,39 @@ namespace ICSharpCode.ILSpyX.Search
|
||||
this.searchRequest = request;
|
||||
this.fullNameSearch = request.FullNameSearch;
|
||||
this.omitGenerics = request.OmitGenerics;
|
||||
this.preparedTerms = PrepareTerms(request.Keywords);
|
||||
}
|
||||
|
||||
static PreparedTerm[] PrepareTerms(string[] keywords)
|
||||
{
|
||||
var result = new List<PreparedTerm>(keywords.Length);
|
||||
foreach (string term in keywords)
|
||||
{
|
||||
if (string.IsNullOrEmpty(term))
|
||||
continue;
|
||||
switch (term[0])
|
||||
{
|
||||
case '+': // must contain
|
||||
result.Add(new PreparedTerm(TermOperator.Contains, term.Substring(1)));
|
||||
break;
|
||||
case '-': // should not contain
|
||||
if (term.Length > 1)
|
||||
result.Add(new PreparedTerm(TermOperator.NotContains, term.Substring(1)));
|
||||
break;
|
||||
case '=': // exact match
|
||||
if (term.Length > 1)
|
||||
result.Add(new PreparedTerm(TermOperator.Exact, term));
|
||||
break;
|
||||
case '~':
|
||||
if (term.Length > 1)
|
||||
result.Add(new PreparedTerm(TermOperator.Fuzzy, term.Substring(1).ToLowerInvariant()));
|
||||
break;
|
||||
default:
|
||||
result.Add(new PreparedTerm(TermOperator.Contains, term));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
public abstract void Search(MetadataFile module, CancellationToken cancellationToken);
|
||||
@@ -95,39 +157,32 @@ namespace ICSharpCode.ILSpyX.Search
|
||||
return regex.IsMatch(name);
|
||||
}
|
||||
|
||||
for (int i = 0; i < searchTerm.Length; ++i)
|
||||
foreach (var term in preparedTerms)
|
||||
{
|
||||
// How to handle overlapping matches?
|
||||
var term = searchTerm[i];
|
||||
if (string.IsNullOrEmpty(term))
|
||||
continue;
|
||||
string text = name;
|
||||
switch (term[0])
|
||||
switch (term.Operator)
|
||||
{
|
||||
case '+': // must contain
|
||||
term = term.Substring(1);
|
||||
goto default;
|
||||
case '-': // should not contain
|
||||
if (term.Length > 1 && text.IndexOf(term.Substring(1), StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
case TermOperator.NotContains:
|
||||
if (name.IndexOf(term.Text, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
return false;
|
||||
break;
|
||||
case '=': // exact match
|
||||
case TermOperator.Exact:
|
||||
{
|
||||
var equalCompareLength = text.IndexOf('`');
|
||||
var equalCompareLength = name.IndexOf('`');
|
||||
if (equalCompareLength == -1)
|
||||
equalCompareLength = text.Length;
|
||||
equalCompareLength = name.Length;
|
||||
|
||||
if (term.Length > 1 && String.Compare(term, 1, text, 0, Math.Max(term.Length, equalCompareLength),
|
||||
if (String.Compare(term.Text, 1, name, 0, Math.Max(term.Text.Length, equalCompareLength),
|
||||
StringComparison.OrdinalIgnoreCase) != 0)
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '~':
|
||||
if (term.Length > 1 && !IsNoncontiguousMatch(text.ToLower(), term.Substring(1).ToLower()))
|
||||
case TermOperator.Fuzzy:
|
||||
if (!IsNoncontiguousMatch(name, term.Text))
|
||||
return false;
|
||||
break;
|
||||
default:
|
||||
if (text.IndexOf(term, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
if (name.IndexOf(term.Text, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
@@ -135,26 +190,26 @@ namespace ICSharpCode.ILSpyX.Search
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsNoncontiguousMatch(string text, string searchTerm)
|
||||
static bool IsNoncontiguousMatch(ReadOnlySpan<char> text, ReadOnlySpan<char> loweredSearchTerm)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(searchTerm))
|
||||
if (text.IsEmpty || loweredSearchTerm.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var textLength = text.Length;
|
||||
if (searchTerm.Length > textLength)
|
||||
if (loweredSearchTerm.Length > textLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var i = 0;
|
||||
for (int searchIndex = 0; searchIndex < searchTerm.Length;)
|
||||
for (int searchIndex = 0; searchIndex < loweredSearchTerm.Length;)
|
||||
{
|
||||
while (i != textLength)
|
||||
{
|
||||
if (text[i] == searchTerm[searchIndex])
|
||||
if (char.ToLowerInvariant(text[i]) == loweredSearchTerm[searchIndex])
|
||||
{
|
||||
// Check if all characters in searchTerm have been matched
|
||||
if (searchTerm.Length == ++searchIndex)
|
||||
if (loweredSearchTerm.Length == ++searchIndex)
|
||||
return true;
|
||||
i++;
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// 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 System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
using ICSharpCode.Decompiler.Metadata;
|
||||
using ICSharpCode.ILSpyX.Search;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ICSharpCode.ILSpy.Tests.Search;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the term-matching semantics of <see cref="AbstractSearchStrategy.IsMatch"/>:
|
||||
/// plain containment, the +/-/=/~ operators, multi-term conjunction, and the
|
||||
/// handling of degenerate terms.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class IsMatchTests
|
||||
{
|
||||
sealed class ExposingSearchStrategy : AbstractSearchStrategy
|
||||
{
|
||||
public ExposingSearchStrategy(params string[] keywords)
|
||||
: base(new SearchRequest { Keywords = keywords }, new ConcurrentQueue<SearchResult>())
|
||||
{
|
||||
}
|
||||
|
||||
public bool Match(string name) => IsMatch(name);
|
||||
|
||||
public override void Search(MetadataFile module, CancellationToken cancellationToken)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsMatch(string name, params string[] keywords)
|
||||
=> new ExposingSearchStrategy(keywords).Match(name);
|
||||
|
||||
[Test]
|
||||
public void Plain_Term_Matches_Substring_Ignoring_Case()
|
||||
{
|
||||
Assert.That(IsMatch("StringBuilder", "builder"), Is.True);
|
||||
Assert.That(IsMatch("StringBuilder", "STRING"), Is.True);
|
||||
Assert.That(IsMatch("StringBuilder", "Comparer"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Plus_Operator_Requires_The_Term_To_Be_Contained()
|
||||
{
|
||||
Assert.That(IsMatch("Enumerable", "+Enum"), Is.True);
|
||||
Assert.That(IsMatch("Enumerable", "+enumera"), Is.True);
|
||||
Assert.That(IsMatch("List", "+Enum"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Minus_Operator_Excludes_Names_Containing_The_Term()
|
||||
{
|
||||
Assert.That(IsMatch("StringBuilder", "-Builder"), Is.False);
|
||||
Assert.That(IsMatch("StringComparer", "-Builder"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Equals_Operator_Requires_Exact_Name_Match()
|
||||
{
|
||||
Assert.That(IsMatch("String", "=String"), Is.True);
|
||||
Assert.That(IsMatch("String", "=string"), Is.True);
|
||||
Assert.That(IsMatch("StringBuilder", "=String"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Equals_Operator_Compares_Against_The_Backtick_Suffixed_Name()
|
||||
{
|
||||
// The compare window is max(term length incl. '=', chars before '`'), so a
|
||||
// generic type only matches when the term spells out the arity suffix too.
|
||||
Assert.That(IsMatch("List`1", "=List`1"), Is.True);
|
||||
Assert.That(IsMatch("List`1", "=List"), Is.False);
|
||||
Assert.That(IsMatch("List`1", "=Dictionary"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Fuzzy_Operator_Matches_Noncontiguous_Character_Sequences()
|
||||
{
|
||||
Assert.That(IsMatch("StringBuilder", "~sb"), Is.True);
|
||||
Assert.That(IsMatch("StringBuilder", "~strbld"), Is.True);
|
||||
Assert.That(IsMatch("StringBuilder", "~xyz"), Is.False);
|
||||
// Characters must appear in order: 'b' never precedes 's'.
|
||||
Assert.That(IsMatch("StringBuilder", "~bs"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Fuzzy_Operator_Ignores_Case_On_Both_Sides()
|
||||
{
|
||||
Assert.That(IsMatch("StringBuilder", "~SB"), Is.True);
|
||||
Assert.That(IsMatch("stringbuilder", "~STRB"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Fuzzy_Term_Longer_Than_The_Name_Never_Matches()
|
||||
{
|
||||
Assert.That(IsMatch("Ab", "~abc"), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Multiple_Terms_Are_A_Conjunction()
|
||||
{
|
||||
Assert.That(IsMatch("StringBuilder", "String", "Builder"), Is.True);
|
||||
Assert.That(IsMatch("StringBuilder", "String", "-Builder"), Is.False);
|
||||
Assert.That(IsMatch("StringComparer", "String", "-Builder"), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Degenerate_Terms_Match_Everything()
|
||||
{
|
||||
// An empty term is skipped; a bare operator has no payload to test.
|
||||
Assert.That(IsMatch("Anything", ""), Is.True);
|
||||
Assert.That(IsMatch("Anything", "~"), Is.True);
|
||||
Assert.That(IsMatch("Anything", "-"), Is.True);
|
||||
}
|
||||
}
|
||||
@@ -181,13 +181,13 @@ namespace ICSharpCode.ILSpy.Languages
|
||||
output.Write("// ");
|
||||
output.BeginSpan(gray);
|
||||
if (isSingleLine)
|
||||
output.Write(text.Substring(0, startColumn).TrimStart());
|
||||
output.Write(text.AsSpan(0, startColumn).TrimStart());
|
||||
else
|
||||
output.Write(text.Substring(0, startColumn));
|
||||
output.Write(text.AsSpan(0, startColumn));
|
||||
output.EndSpan();
|
||||
output.Write(text.Substring(startColumn, endColumn - startColumn));
|
||||
output.Write(text.AsSpan(startColumn, endColumn - startColumn));
|
||||
output.BeginSpan(gray);
|
||||
output.Write(text.Substring(endColumn));
|
||||
output.Write(text.AsSpan(endColumn));
|
||||
output.EndSpan();
|
||||
output.WriteLine();
|
||||
}
|
||||
|
||||
@@ -186,6 +186,13 @@ namespace ICSharpCode.ILSpy.TextView
|
||||
CheckLength();
|
||||
}
|
||||
|
||||
public void Write(ReadOnlySpan<char> text)
|
||||
{
|
||||
WriteIndentIfNeeded();
|
||||
builder.Append(text);
|
||||
CheckLength();
|
||||
}
|
||||
|
||||
public void WriteLine()
|
||||
{
|
||||
if (IgnoreNewLineAndIndent)
|
||||
|
||||
@@ -48,6 +48,13 @@ namespace ICSharpCode.ILSpy.TextView
|
||||
void BeginSpan(HighlightingColor highlightingColor);
|
||||
void EndSpan();
|
||||
|
||||
/// <summary>
|
||||
/// Writes a slice of text without requiring the caller to allocate an intermediate
|
||||
/// string. Implementations that buffer internally should override the default,
|
||||
/// which falls back to <see cref="ITextOutput.Write(string)"/>.
|
||||
/// </summary>
|
||||
void Write(ReadOnlySpan<char> text) => Write(text.ToString());
|
||||
|
||||
/// <summary>
|
||||
/// Title displayed in the document tab's header.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user