Browse Source

Add fine-grained C# AST debug steps

Record AST transform groups and mutation steps through the C# pipeline, replay selected steps with the stepper, and carry modified-node ranges through output so the Debug Steps pane can highlight the selected mutation without replacing its full step tree.

Assisted-by: CodeAlta:gpt-5.5:CodeAlta
pull/3847/head
Siegfried Pammer 2 weeks ago committed by Siegfried Pammer
parent
commit
d3517739ff
  1. 11
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs
  2. 26
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  3. 33
      ICSharpCode.Decompiler/CSharp/Transforms/AddCheckedBlocks.cs
  4. 1
      ICSharpCode.Decompiler/CSharp/Transforms/AddXmlDocumentationTransform.cs
  5. 20
      ICSharpCode.Decompiler/CSharp/Transforms/CombineQueryExpressions.cs
  6. 97
      ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs
  7. 7
      ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs
  8. 2
      ICSharpCode.Decompiler/CSharp/Transforms/FixNameCollisions.cs
  9. 1
      ICSharpCode.Decompiler/CSharp/Transforms/FlattenSwitchBlocks.cs
  10. 9
      ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs
  11. 16
      ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs
  12. 22
      ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUnsafeModifier.cs
  13. 32
      ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs
  14. 12
      ICSharpCode.Decompiler/CSharp/Transforms/NormalizeBlockStatements.cs
  15. 24
      ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs
  16. 11
      ICSharpCode.Decompiler/CSharp/Transforms/PrettifyAssignments.cs
  17. 62
      ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs
  18. 71
      ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs
  19. 30
      ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs
  20. 47
      ICSharpCode.Decompiler/IL/Transforms/Stepper.cs
  21. 93
      ILSpy.Tests/Views/DebugStepsTests.cs
  22. 7
      ILSpy/DecompilationOptions.cs
  23. 9
      ILSpy/Languages/CSharpHighlightingTokenWriter.cs
  24. 29
      ILSpy/Languages/CSharpLanguage.DebugSteps.cs
  25. 89
      ILSpy/Languages/CSharpLanguage.cs
  26. 20
      ILSpy/TextView/AvaloniaEditTextOutput.cs
  27. 12
      ILSpy/TextView/DecompilerTabPageModel.cs
  28. 30
      ILSpy/TextView/DecompilerTextView.axaml.cs
  29. 50
      ILSpy/TextView/NodeLookup.cs
  30. 22
      ILSpy/TextView/TextRange.cs
  31. 10
      ILSpy/ViewModels/DebugStepsPaneModel.cs

11
ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs

@ -81,5 +81,16 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -81,5 +81,16 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
func();
func2();
}
public static void CapturedBoolResult(Dictionary<int, int> d, int key)
{
// The boolean result of the out-returning call is captured into a local, yet the out
// parameter is still promoted to an inline 'out var' rather than a separate declaration.
bool value = d.TryGetValue(key, out var value2);
Console.WriteLine(value);
Console.WriteLine(value);
Console.WriteLine(value2);
Console.WriteLine(value2);
}
}
}

26
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

@ -176,6 +176,8 @@ namespace ICSharpCode.Decompiler.CSharp @@ -176,6 +176,8 @@ namespace ICSharpCode.Decompiler.CSharp
List<IAstTransform> astTransforms = GetAstTransforms();
public Stepper Stepper { get; set; } = new Stepper();
/// <summary>
/// Returns all built-in transforms of the C# AST pipeline.
/// </summary>
@ -714,17 +716,27 @@ namespace ICSharpCode.Decompiler.CSharp @@ -714,17 +716,27 @@ namespace ICSharpCode.Decompiler.CSharp
void RunTransforms(AstNode rootNode, DecompileRun decompileRun, ITypeResolveContext decompilationContext)
{
var typeSystemAstBuilder = CreateAstBuilder(decompileRun.Settings);
var context = new TransformContext(typeSystem, decompileRun, decompilationContext, typeSystemAstBuilder);
var context = new TransformContext(typeSystem, decompileRun, decompilationContext, typeSystemAstBuilder) {
Stepper = Stepper
};
// The tree handed to the pipeline must already be well-formed; check it once up front so a
// malformed builder output is caught here rather than blamed on the first transform (DEBUG only).
rootNode.CheckInvariant();
foreach (var transform in astTransforms)
try
{
foreach (var transform in astTransforms)
{
CancellationToken.ThrowIfCancellationRequested();
context.StepStartGroup(transform.GetType().Name);
transform.Run(rootNode, context);
// Verify the slot structure survived the transform (DEBUG only); mirrors the IL
// pipeline's per-transform ILInstruction.CheckInvariant.
rootNode.CheckInvariant();
context.StepEndGroup(keepIfEmpty: true);
}
}
catch (StepLimitReachedException)
{
CancellationToken.ThrowIfCancellationRequested();
transform.Run(rootNode, context);
// Verify the slot structure survived the transform (DEBUG only); mirrors the IL
// pipeline's per-transform ILInstruction.CheckInvariant.
rootNode.CheckInvariant();
}
CancellationToken.ThrowIfCancellationRequested();
rootNode.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true });

33
ICSharpCode.Decompiler/CSharp/Transforms/AddCheckedBlocks.cs

@ -155,7 +155,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -155,7 +155,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return new InsertedNodeList(a, b);
}
public abstract void Insert();
public abstract void Insert(TransformContext context);
}
class InsertedNodeList : InsertedNode
@ -168,10 +168,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -168,10 +168,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
this.child2 = child2;
}
public override void Insert()
public override void Insert(TransformContext context)
{
child1.Insert();
child2.Insert();
child1.Insert(context);
child2.Insert(context);
}
}
@ -186,12 +186,15 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -186,12 +186,15 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
this.isChecked = isChecked;
}
public override void Insert()
public override void Insert(TransformContext context)
{
context.Step(isChecked ? "Add checked expression" : "Add unchecked expression", expression);
Expression? replacement;
if (isChecked)
expression.ReplaceWith(e => new CheckedExpression { Expression = e });
replacement = expression.ReplaceWith(e => new CheckedExpression { Expression = e });
else
expression.ReplaceWith(e => new UncheckedExpression { Expression = e });
replacement = expression.ReplaceWith(e => new UncheckedExpression { Expression = e });
context.EndStep(replacement);
}
}
@ -208,11 +211,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -208,11 +211,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
this.isChecked = isChecked;
}
public override void Insert()
public override void Insert(TransformContext context)
{
// An InsertedBlock with a null start has infinite cost in the search and is never
// selected for insertion, so by the time Insert runs firstStatement is non-null.
Debug.Assert(firstStatement != null);
context.Step(isChecked ? "Add checked block" : "Add unchecked block", firstStatement);
BlockStatement newBlock = new BlockStatement();
// Move all statements except for the first
Statement? next;
@ -222,12 +226,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -222,12 +226,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
newBlock.Add(stmt.Detach());
}
// Replace the first statement with the new (un)checked block
if (isChecked)
firstStatement.ReplaceWith(new CheckedStatement { Body = newBlock });
else
firstStatement.ReplaceWith(new UncheckedStatement { Body = newBlock });
Statement checkedBlock = isChecked
? new CheckedStatement { Body = newBlock }
: new UncheckedStatement { Body = newBlock };
firstStatement.ReplaceWith(checkedBlock);
// now also move the first node into the new block
newBlock.Statements.InsertAfter(null, firstStatement);
context.EndStep(checkedBlock);
}
}
#endregion
@ -260,11 +265,11 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -260,11 +265,11 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
Result r = GetResultFromBlock(block);
if (context.DecompileRun.Settings.CheckForOverflowUnderflow)
{
r.NodesToInsertInCheckedContext?.Insert();
r.NodesToInsertInCheckedContext?.Insert(context);
}
else
{
r.NodesToInsertInUncheckedContext?.Insert();
r.NodesToInsertInUncheckedContext?.Insert(context);
}
}
}

1
ICSharpCode.Decompiler/CSharp/Transforms/AddXmlDocumentationTransform.cs

@ -49,6 +49,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -49,6 +49,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
string doc = provider.GetDocumentation(entity);
if (doc != null)
{
context.Step("Add XML documentation", entityDecl);
InsertXmlDocumentation(entityDecl, new StringReader(doc));
}
}

20
ICSharpCode.Decompiler/CSharp/Transforms/CombineQueryExpressions.cs

@ -19,6 +19,7 @@ @@ -19,6 +19,7 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ICSharpCode.Decompiler.CSharp.Syntax;
@ -32,11 +33,21 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -32,11 +33,21 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
/// </summary>
public class CombineQueryExpressions : IAstTransform
{
[AllowNull] TransformContext context;
public void Run(AstNode rootNode, TransformContext context)
{
if (!context.Settings.QueryExpressions)
return;
CombineQueries(rootNode, new Dictionary<string, object?>());
this.context = context;
try
{
CombineQueries(rootNode, new Dictionary<string, object?>());
}
finally
{
this.context = null;
}
}
static readonly InvocationExpression castPattern = new InvocationExpression {
@ -68,10 +79,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -68,10 +79,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
else
{
QueryContinuationClause continuation = new QueryContinuationClause();
context.Step("Introduce query continuation", fromClause);
continuation.PrecedingQuery = innerQuery.Detach();
continuation.Identifier = fromClause.Identifier;
continuation.CopyAnnotationsFrom(fromClause);
fromClause.ReplaceWith(continuation);
context.EndStep(continuation);
}
}
else
@ -79,6 +92,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -79,6 +92,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
Match m = castPattern.Match(fromClause.Expression);
if (m.Success)
{
context.Step("Move Cast type into from clause", fromClause);
fromClause.Type = m.Get<AstType>("targetType").Single().Detach();
fromClause.Expression = m.Get<Expression>("inExpr").Single().Detach();
}
@ -117,6 +131,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -117,6 +131,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// from * in (from x in ... select new { members of anonymous type }) ...
// =>
// from x in ... { let x = ... } ...
context.Step("Remove transparent query identifier", fromClause);
fromClause.Remove();
selectClause.Remove();
// Move clauses from innerQuery to query
@ -125,6 +140,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -125,6 +140,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
query.Clauses.InsertAfter(insertionPos, insertionPos = clause.Detach());
}
context.EndStep(query.Clauses.First());
foreach (var expr in match.Get<Expression>("expr"))
{
@ -176,7 +192,9 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -176,7 +192,9 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
newIdent.RemoveAnnotations<Semantics.MemberResolveResult>(); // remove the reference to the property of the anonymous type
if (fromOrLetIdentifiers.TryGetValue(mre.MemberName, out var annotation) && annotation != null)
newIdent.AddAnnotation(annotation);
context.Step("Replace transparent query identifier reference", mre);
mre.ReplaceWith(newIdent);
context.EndStep(newIdent);
return;
}
}

97
ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs

@ -213,6 +213,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -213,6 +213,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
if (stmt.Expression is DirectionExpression dir && IsValidInStatementExpression(dir.Expression))
{
context.Step("Unwrap direction expression statement", stmt);
stmt.Expression = dir.Expression.Detach();
}
else if (!IsValidInStatementExpression(stmt.Expression))
@ -222,12 +223,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -222,12 +223,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// if possible use C# 7.0 discard-assignment
if (context.Settings.Discards && !ExpressionBuilder.HidesVariableWithName(function, "_"))
{
context.Step("Assign invalid expression statement to discard", stmt);
stmt.Expression = new AssignmentExpression(
new IdentifierExpression("_"), // no ResolveResult
stmt.Expression.Detach());
}
else
{
context.Step("Assign invalid expression statement to temporary", stmt);
// assign result to dummy variable
var type = stmt.Expression.GetResolveResult().Type;
var v = function.RegisterVariable(
@ -542,7 +545,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -542,7 +545,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
continue;
var designation = StatementBuilder.TranslateDeconstructionDesignation(deconstruct, isForeach: false);
left.ReplaceWith(new DeclarationExpression { Type = new SimpleType("var"), Designation = designation });
context.Step("Declare deconstruction variables", left);
var declarationExpression = new DeclarationExpression { Type = new SimpleType("var"), Designation = designation };
left.ReplaceWith(declarationExpression);
context.EndStep(declarationExpression);
foreach (var v in usedVariables)
{
@ -595,7 +601,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -595,7 +601,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
void InsertVariableDeclarations(TransformContext context)
{
var replacements = new List<(AstNode, AstNode)>();
var replacements = new List<(AstNode OldNode, Func<AstNode> CreateNewNode, string StepDescription)>();
foreach (var (ilVariable, v) in variableDict)
{
if (v.RemovedDueToCollision || v.DeclaredInDeconstruction)
@ -621,17 +627,19 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -621,17 +627,19 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
type.AddTrailingTrivia(new Comment("pinned", CommentType.MultiLine));
}
var vds = new VariableDeclarationStatement(type, v.Name, assignment.Right.Detach());
var init = vds.Variables.Single();
init.AddAnnotation(assignment.Left.GetResolveResult());
foreach (object annotation in assignment.Left.Annotations.Concat(assignment.Annotations))
{
if (!(annotation is ResolveResult))
replacements.Add((v.InsertionPoint.nextNode, () => {
var vds = new VariableDeclarationStatement(type, v.Name, assignment.Right.Detach());
var init = vds.Variables.Single();
init.AddAnnotation(assignment.Left.GetResolveResult());
foreach (object annotation in assignment.Left.Annotations.Concat(assignment.Annotations))
{
init.AddAnnotation(annotation);
if (!(annotation is ResolveResult))
{
init.AddAnnotation(annotation);
}
}
}
replacements.Add((v.InsertionPoint.nextNode, vds));
return vds;
}, "Combine variable declaration with initializer"));
}
else if (CanBeDeclaredAsOutVariable(v, out var dirExpr))
{
@ -675,7 +683,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -675,7 +683,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
ovd.RemoveAnnotations<ResolveResult>();
ovd.AddAnnotation(new OutVarResolveResult(v.Type));
}
replacements.Add((dirExpr, ovd));
replacements.Add((dirExpr, () => ovd, "Declare out variable"));
}
else
{
@ -688,6 +696,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -688,6 +696,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
}
var vds = new VariableDeclarationStatement(type, v.Name, initializer);
vds.Variables.Single().AddAnnotation(new ILVariableResolveResult(ilVariable));
context.Step("Insert variable declaration", v.InsertionPoint.nextNode);
if (v.InsertionPoint.nextNode.Parent is LambdaExpression lambda)
{
Debug.Assert(lambda.Body is not BlockStatement);
@ -708,24 +717,27 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -708,24 +717,27 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
AstType unsafeType = context.TypeSystemAstBuilder.ConvertType(
context.TypeSystem.FindType(KnownTypeCode.Unsafe));
AstNode insertedNode;
if (context.Settings.OutVariables)
{
var outVarDecl = new OutVarDeclarationExpression(type.Clone(), v.Name);
outVarDecl.Variable.AddAnnotation(new ILVariableResolveResult(ilVariable));
var skipInitStatement = new ExpressionStatement {
Expression = new InvocationExpression {
Target = new MemberReferenceExpression {
Target = new TypeReferenceExpression(unsafeType),
MemberName = "SkipInit"
},
Arguments = {
outVarDecl
}
}
};
insertionParent.InsertChildBefore(
v.InsertionPoint.nextNode,
new ExpressionStatement {
Expression = new InvocationExpression {
Target = new MemberReferenceExpression {
Target = new TypeReferenceExpression(unsafeType),
MemberName = "SkipInit"
},
Arguments = {
outVarDecl
}
}
},
skipInitStatement,
Slots.Statement);
insertedNode = skipInitStatement;
}
else
{
@ -733,25 +745,28 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -733,25 +745,28 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
v.InsertionPoint.nextNode,
vds,
Slots.Statement);
insertedNode = vds;
var skipInitStatement = new ExpressionStatement {
Expression = new InvocationExpression {
Target = new MemberReferenceExpression {
Target = new TypeReferenceExpression(unsafeType),
MemberName = "SkipInit"
},
Arguments = {
new DirectionExpression(
FieldDirection.Out,
new IdentifierExpression(v.Name)
.WithRR(new ILVariableResolveResult(ilVariable))
)
}
}
};
insertionParent.InsertChildBefore(
v.InsertionPoint.nextNode,
new ExpressionStatement {
Expression = new InvocationExpression {
Target = new MemberReferenceExpression {
Target = new TypeReferenceExpression(unsafeType),
MemberName = "SkipInit"
},
Arguments = {
new DirectionExpression(
FieldDirection.Out,
new IdentifierExpression(v.Name)
.WithRR(new ILVariableResolveResult(ilVariable))
)
}
}
},
skipInitStatement,
Slots.Statement);
}
context.EndStep(insertedNode);
}
else
{
@ -759,13 +774,17 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -759,13 +774,17 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
v.InsertionPoint.nextNode,
vds,
Slots.Statement);
context.EndStep(vds);
}
}
}
// perform replacements at end, so that we don't replace a node while it is still referenced by a VariableToDeclare
foreach (var (oldNode, newNode) in replacements)
foreach (var (oldNode, createNewNode, stepDescription) in replacements)
{
context.Step(stepDescription, oldNode);
var newNode = createNewNode();
oldNode.ReplaceWith(newNode);
context.EndStep(newNode);
}
}

7
ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs

@ -56,7 +56,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -56,7 +56,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
foreach (var ident in rootNode.DescendantsAndSelf.OfType<Identifier>())
{
ident.Name = ReplaceInvalid(ident.Name);
string newName = ReplaceInvalid(ident.Name);
if (newName != ident.Name)
{
context.Step($"Escape identifier '{ident.Name}'", ident);
ident.Name = newName;
}
}
}
}

2
ICSharpCode.Decompiler/CSharp/Transforms/FixNameCollisions.cs

@ -56,6 +56,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -56,6 +56,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (memberNames.Contains(oldName) && symbol is IField { Accessibility: Accessibility.Private })
{
string newName = PickNewName(memberNames, oldName);
context.Step($"Rename field '{oldName}' to '{newName}'", fieldDecl);
fieldDecl.Variables.Single().Name = newName;
renamedSymbols[symbol] = newName;
}
@ -70,6 +71,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -70,6 +71,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (symbol != null && renamedSymbols.TryGetValue(symbol, out string? newName))
{
// An IdentifierExpression / MemberReferenceExpression always carries its name identifier.
context.Step($"Rename field reference to '{newName}'", node);
node.GetChild(Slots.Identifier)!.Name = newName;
}
}

1
ICSharpCode.Decompiler/CSharp/Transforms/FlattenSwitchBlocks.cs

@ -22,6 +22,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -22,6 +22,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (blockStatement == null || blockStatement.Statements.Any(ContainsLocalDeclaration))
continue;
context.Step("Flatten switch section block", blockStatement);
blockStatement.Remove();
blockStatement.Statements.MoveTo(switchSection.Statements);
}

9
ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs

@ -108,10 +108,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -108,10 +108,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return;
}
var method = (IMethod)invocationExpression.GetSymbol()!;
bool stepped = false;
if (firstArgument is DirectionExpression dirExpr)
{
if (!context.Settings.RefExtensionMethods || dirExpr.FieldDirection == FieldDirection.Out)
return;
context.Step("Introduce extension method call", invocationExpression);
stepped = true;
// A ref/out direction expression always wraps an operand.
firstArgument = dirExpr.Expression!;
target = firstArgument.GetResolveResult();
@ -120,17 +123,23 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -120,17 +123,23 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
else if (firstArgument is NullReferenceExpression)
{
Debug.Assert(context.RequiredNamespacesSuperset.Contains(method.Parameters[0].Type.Namespace));
context.Step("Introduce extension method call", invocationExpression);
stepped = true;
// The replacement is a freshly created CastExpression, so the result is non-null.
firstArgument = firstArgument.ReplaceWith(expr => new CastExpression(context.TypeSystemAstBuilder.ConvertType(method.Parameters[0].Type), expr.Detach()))!;
}
if (invocationExpression.Target is IdentifierExpression identifierExpression)
{
if (!stepped)
context.Step("Introduce extension method call", invocationExpression);
identifierExpression.Detach();
memberRefExpr = new MemberReferenceExpression(firstArgument.Detach(), method.Name, identifierExpression.TypeArguments.Detach());
invocationExpression.Target = memberRefExpr;
}
else
{
if (!stepped)
context.Step("Introduce extension method call", invocationExpression);
// The target is not an IdentifierExpression, so CanTransformToExtensionMethodCall
// matched the MemberReferenceExpression case and memberRefExpr is non-null.
memberRefExpr!.Target = firstArgument.Detach();

16
ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs

@ -51,6 +51,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -51,6 +51,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (IsDegenerateQuery(query))
{
// introduce select for degenerate query
context.Step("Add degenerate query select clause", query);
query.Clauses.Add(new QuerySelectClause { Expression = new IdentifierExpression(fromClause.Identifier).CopyAnnotationsFrom(fromClause) });
}
// See if the data source of this query is a degenerate query,
@ -61,6 +62,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -61,6 +62,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
QueryFromClause innerFromClause = (QueryFromClause)innerQuery.Clauses.First();
ILVariable? innerVariable = innerFromClause.Annotation<ILVariableResolveResult>()?.Variable;
ILVariable? rangeVariable = fromClause.Annotation<ILVariableResolveResult>()?.Variable;
context.Step("Combine nested query clauses", fromClause);
// Replace the fromClause with all clauses from the inner query
fromClause.Remove();
QueryClause? insertionPos = null;
@ -69,6 +71,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -69,6 +71,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
CombineRangeVariables(clause, innerVariable, rangeVariable);
query.Clauses.InsertAfter(insertionPos, insertionPos = clause.Detach());
}
context.EndStep(innerFromClause);
fromClause = innerFromClause;
innerQuery = fromClause.Expression as QueryExpression;
}
@ -87,9 +90,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -87,9 +90,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
var variable = parent.Annotation<ILVariableResolveResult>()?.Variable;
if (variable == oldVariable)
{
context.Step("Combine query range variables", identifier);
parent.RemoveAnnotations<ILVariableResolveResult>();
parent.AddAnnotation(new ILVariableResolveResult(newVariable));
identifier.ReplaceWith(Identifier.Create(newVariable.Name!));
var newIdentifier = Identifier.Create(newVariable.Name!);
identifier.ReplaceWith(newIdentifier);
context.EndStep(newIdentifier);
}
}
}
@ -110,6 +116,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -110,6 +116,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (node.Parent is ExpressionStatement && CanUseDiscardAssignment())
query = new AssignmentExpression(new IdentifierExpression("_"), query);
node.ReplaceWith(query);
context.EndStep(query);
}
AstNode? next;
@ -145,6 +152,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -145,6 +152,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
Expression expr = invocation.Arguments.Single();
if (MatchSimpleLambda(expr, out var parameter, out var body))
{
context.Step("Build select query", invocation);
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach()));
query.Clauses.Add(new QuerySelectClause { Expression = WrapExpressionInParenthesesIfNecessary(body.Detach(), parameter.Name!) }.CopyAnnotationsFrom(expr));
@ -162,6 +170,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -162,6 +170,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
&& MatchSimpleLambda(projectionLambda, out var parameter2, out var elementSelector)
&& parameter1.Name == parameter2.Name)
{
context.Step("Build group query", invocation);
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(parameter1, mre.Target.Detach()));
var queryGroupClause = new QueryGroupClause {
@ -179,6 +188,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -179,6 +188,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
Expression lambda = invocation.Arguments.Single();
if (MatchSimpleLambda(lambda, out var parameter, out var keySelector))
{
context.Step("Build group query", invocation);
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach()));
query.Clauses.Add(new QueryGroupClause { Projection = new IdentifierExpression(parameter.Name!).CopyAnnotationsFrom(parameter), Key = keySelector.Detach() });
@ -203,6 +213,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -203,6 +213,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
ParameterDeclaration p2 = lambda.Parameters.ElementAt(1);
if (p1.Name == parameter.Name)
{
context.Step("Build select-many query", invocation);
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(p1, mre.Target.Detach()));
query.Clauses.Add(MakeFromClause(p2, collectionSelector.Detach()).CopyAnnotationsFrom(fromExpressionLambda));
@ -221,6 +232,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -221,6 +232,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
Expression expr = invocation.Arguments.Single();
if (MatchSimpleLambda(expr, out var parameter, out var body))
{
context.Step("Build where query", invocation);
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach()));
query.Clauses.Add(new QueryWhereClause { Condition = body.Detach() }.CopyAnnotationsFrom(expr));
@ -242,6 +254,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -242,6 +254,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
if (ValidateThenByChain(invocation, parameter.Name!))
{
context.Step("Build order query", invocation);
QueryOrderClause orderClause = new QueryOrderClause();
while (mre.MemberName == "ThenBy" || mre.MemberName == "ThenByDescending")
{
@ -302,6 +315,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -302,6 +315,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (ValidateParameter(p1) && ValidateParameter(p2)
&& p1.Name == element1.Name && (p2.Name == element2.Name || mre.MemberName == "GroupJoin"))
{
context.Step(mre.MemberName == "GroupJoin" ? "Build group join query" : "Build join query", invocation);
QueryExpression query = new QueryExpression();
query.Clauses.Add(MakeFromClause(element1, source1.Detach()));
QueryJoinClause joinClause = new QueryJoinClause();

22
ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUnsafeModifier.cs

@ -29,9 +29,19 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -29,9 +29,19 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
public class IntroduceUnsafeModifier : DepthFirstAstVisitor<bool>, IAstTransform
{
TransformContext? context;
public void Run(AstNode compilationUnit, TransformContext context)
{
compilationUnit.AcceptVisitor(this);
this.context = context;
try
{
compilationUnit.AcceptVisitor(this);
}
finally
{
this.context = null;
}
}
public static bool IsUnsafe(AstNode node)
@ -52,6 +62,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -52,6 +62,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
}
if (result && node is EntityDeclaration && !(node is Accessor))
{
if (context != null)
context.Step("Add unsafe modifier", node);
((EntityDeclaration)node).Modifiers |= Modifiers.Unsafe;
return false;
}
@ -95,6 +107,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -95,6 +107,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
&& bop.GetResolveResult() is OperatorResolveResult orr
&& orr.Operands.FirstOrDefault()?.Type.Kind == TypeKind.Pointer)
{
if (context != null)
context.Step("Replace pointer addition with indexer", unaryOperatorExpression);
// transform "*(ptr + int)" to "ptr[int]"
IndexerExpression indexer = new IndexerExpression();
indexer.Target = bop.Left!.Detach();
@ -102,6 +116,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -102,6 +116,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
indexer.CopyAnnotationsFrom(unaryOperatorExpression);
indexer.CopyAnnotationsFrom(bop);
unaryOperatorExpression.ReplaceWith(indexer);
if (context != null)
context.EndStep(indexer);
}
return true;
}
@ -121,6 +137,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -121,6 +137,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
UnaryOperatorExpression? uoe = memberReferenceExpression.Target as UnaryOperatorExpression;
if (uoe != null && uoe.Operator == UnaryOperatorType.Dereference)
{
if (context != null)
context.Step("Replace pointer member access", memberReferenceExpression);
PointerReferenceExpression pre = new PointerReferenceExpression();
pre.Target = uoe.Expression.Detach();
pre.MemberName = memberReferenceExpression.MemberName;
@ -129,6 +147,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -129,6 +147,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
pre.RemoveAnnotations<ResolveResult>(); // only copy the ResolveResult from the MRE
pre.CopyAnnotationsFrom(memberReferenceExpression);
memberReferenceExpression.ReplaceWith(pre);
if (context != null)
context.EndStep(pre);
}
if (HasUnsafeResolveResult(memberReferenceExpression))
return true;

32
ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs

@ -26,6 +26,9 @@ using System.Linq; @@ -26,6 +26,9 @@ using System.Linq;
using ICSharpCode.Decompiler.CSharp.Resolver;
using ICSharpCode.Decompiler.CSharp.Syntax;
#if STEP
using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching;
#endif
using ICSharpCode.Decompiler.CSharp.TypeSystem;
using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.Semantics;
@ -73,7 +76,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -73,7 +76,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
resolvedNamespaces.Add(resolvedNamespace);
}
rootNode.InsertChildAfter(insertionPoint, new UsingDeclaration { Import = nsType }, Slots.Member);
context.Step("Add using declaration", rootNode);
var node = new UsingDeclaration { Import = nsType };
rootNode.InsertChildAfter(insertionPoint, node, Slots.Member);
context.EndStep(node);
}
}
@ -189,6 +195,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -189,6 +195,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
readonly bool ignoreUsingScope;
readonly DecompilerSettings settings;
readonly TransformContext context;
CSharpResolver resolver;
TypeSystemAstBuilder astBuilder;
@ -197,6 +204,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -197,6 +204,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
public FullyQualifyAmbiguousTypeNamesVisitor(TransformContext context, UsingScope usingScope)
{
this.context = context;
this.ignoreUsingScope = !context.Settings.UsingDeclarations;
this.settings = context.Settings;
this.resolver = new CSharpResolver(new CSharpTypeResolveContext(context.TypeSystem.MainModule));
@ -401,13 +409,31 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -401,13 +409,31 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
}
if (simpleType.Parent is Syntax.Attribute)
{
simpleType.ReplaceWith(astBuilder.ConvertAttributeType(rr.Type));
ReplaceAndRecordStep("Qualify ambiguous attribute type", simpleType, astBuilder.ConvertAttributeType(rr.Type));
}
else
{
simpleType.ReplaceWith(astBuilder.ConvertType(rr.Type));
ReplaceAndRecordStep("Qualify ambiguous type", simpleType, astBuilder.ConvertType(rr.Type));
}
}
void ReplaceAndRecordStep(string stepDescription, SimpleType simpleType, AstType replacement)
{
#if STEP
// Record a debug step only when the converted type actually differs from the
// original, so the step list stays meaningful. This structural comparison is
// debug-only (it allocates a pattern Match); release builds skip it entirely and
// just perform the replacement below.
bool changed = !simpleType.IsMatch(replacement);
if (changed)
context.Step(stepDescription, simpleType);
#endif
simpleType.ReplaceWith(replacement);
#if STEP
if (changed)
context.EndStep(replacement);
#endif
}
}
}
}

12
ICSharpCode.Decompiler/CSharp/Transforms/NormalizeBlockStatements.cs

@ -26,6 +26,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -26,6 +26,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
base.VisitSyntaxTree(syntaxTree);
if (context.Settings.FileScopedNamespaces && singleNamespaceDeclaration != null)
{
context.Step("Use file-scoped namespace", singleNamespaceDeclaration);
singleNamespaceDeclaration.IsFileScoped = true;
}
}
@ -106,7 +107,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -106,7 +107,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
if (statement is BlockStatement b && b.Statements.Count == 1 && IsAllowedAsEmbeddedStatement(b.Statements.First(), parent))
{
statement.ReplaceWith(b.Statements.First().Detach());
context.Step("Remove redundant block statement", statement);
var innerStatement = b.Statements.First().Detach();
statement.ReplaceWith(innerStatement);
context.EndStep(innerStatement);
}
else if (!IsAllowedAsEmbeddedStatement(statement, parent))
{
@ -120,11 +124,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -120,11 +124,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return parent is IfElseStatement && statement.Slot?.Kind == Slots.FalseStatement;
}
static void InsertBlock(Statement statement)
void InsertBlock(Statement statement)
{
if (!(statement is BlockStatement))
{
var b = new BlockStatement();
context.Step("Add block statement", statement);
statement.ReplaceWith(b);
if (statement is EmptyStatement && !statement.HasChildren)
{
@ -134,6 +139,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -134,6 +139,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
b.Add(statement);
}
context.EndStep(b);
}
}
@ -221,6 +227,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -221,6 +227,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return;
if ((getter.Modifiers & ~movableModifiers) != 0)
return;
context.Step("Use expression-bodied property", propertyDeclaration);
propertyDeclaration.Modifiers |= getter.Modifiers;
propertyDeclaration.ExpressionBody = m.Get<Expression>("expression").Single().Detach();
propertyDeclaration.CopyAnnotationsFrom(getter);
@ -236,6 +243,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -236,6 +243,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return;
if ((getter.Modifiers & ~movableModifiers) != 0)
return;
context.Step("Use expression-bodied indexer", indexerDeclaration);
indexerDeclaration.Modifiers |= getter.Modifiers;
indexerDeclaration.ExpressionBody = m.Get<Expression>("expression").Single().Detach();
indexerDeclaration.CopyAnnotationsFrom(getter);

24
ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs

@ -191,6 +191,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -191,6 +191,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return null;
if (next is ForStatement forStatement && ForStatementUsesVariable(forStatement, variable))
{
context.Step("Move declaration into for initializer", node);
node.Remove();
next.InsertChildAfter(null, node, Slots.ForInitializer);
return (ForStatement)next;
@ -212,6 +213,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -212,6 +213,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// Whereas continue in for jumps to the increment block.
if (loop.DescendantNodes(DescendIntoStatement).OfType<Statement>().Any(s => s is ContinueStatement))
return null;
context.Step("Transform while loop to for", loop);
node.Remove();
BlockStatement newBody = new BlockStatement();
foreach (Statement stmt in m3.Get<Statement>("statement"))
@ -223,6 +225,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -223,6 +225,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
forStatement.Iterators.Add(iteratorStatement.Detach());
forStatement.EmbeddedStatement = newBody;
loop.ReplaceWith(forStatement);
context.EndStep(forStatement);
return forStatement;
}
@ -363,6 +366,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -363,6 +366,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return null;
if (indexVariable.StoreCount != 2 || indexVariable.LoadCount != 3 || indexVariable.AddressCount != 0)
return null;
context.Step("Introduce foreach over array", forStatement);
var body = new BlockStatement();
foreach (var statement in m.Get<Statement>("statements"))
body.Statements.Add(statement.Detach());
@ -378,6 +382,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -378,6 +382,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type));
// TODO : add ForeachAnnotation
forStatement.ReplaceWith(foreachStmt);
context.EndStep(foreachStmt);
return foreachStmt;
}
@ -533,6 +538,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -533,6 +538,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
|| !upperBounds.All(ub => ub.IsSingleDefinition && ub.LoadCount == 1)
|| !lowerBounds.All(lb => lb.StoreCount == 2 && lb.LoadCount == 3 && lb.AddressCount == 0))
return null;
context.Step("Introduce foreach over multidimensional array", expressionStatement);
var body = new BlockStatement();
foreach (var statement in statements)
body.Statements.Add(statement.Detach());
@ -550,6 +556,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -550,6 +556,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type));
// TODO : add ForeachAnnotation
expressionStatement.ReplaceWith(foreachStmt);
context.EndStep(foreachStmt);
return foreachStmt;
}
@ -643,6 +650,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -643,6 +650,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return null;
if (field.IsCompilerGenerated() && field.DeclaringTypeDefinition == property.DeclaringTypeDefinition)
{
context.Step("Convert property to auto-property", propertyDeclaration);
// Clearing the accessor body turns it into an auto-property accessor.
var getter = propertyDeclaration.Getter;
var setter = propertyDeclaration.Setter;
@ -710,6 +718,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -710,6 +718,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (newIdentifier != null)
{
identifier.ReplaceWith(newIdentifier);
context.EndStep(newIdentifier);
return newIdentifier;
}
}
@ -769,6 +778,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -769,6 +778,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
if (!property.CanSet && !context.Settings.GetterOnlyAutomaticProperties)
return null;
context.Step("Replace backing field use with property", identifier);
parent.RemoveAnnotations<MemberResolveResult>();
parent.AddAnnotation(new MemberResolveResult(mrr.TargetResult, property));
return Identifier.Create(property.Name);
@ -793,6 +803,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -793,6 +803,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
var eventDef = module.ResolveEntity(eventHandle) as IEvent;
if (eventDef != null && currentMethod?.AccessorOwner != eventDef)
{
context.Step("Replace event backing field use with event", identifier);
parent.RemoveAnnotations<MemberResolveResult>();
parent.AddAnnotation(new MemberResolveResult(mrr.TargetResult, eventDef));
identifier.Name = eventDef.Name;
@ -1026,6 +1037,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -1026,6 +1037,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
}
if (ev.AddAccessor is not { } addAccessor)
return null;
context.Step("Convert custom event to field-like event", ev);
RemoveCompilerGeneratedAttribute(addAccessor.Attributes, attributeTypesToRemoveFromAutoEvents);
EventDeclaration ed = new EventDeclaration();
ev.Attributes.MoveTo(ed.Attributes);
@ -1054,6 +1066,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -1054,6 +1066,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
}
ev.ReplaceWith(ed);
context.EndStep(ed);
return ed;
bool IsEventBackingField(FieldDeclaration fd)
@ -1094,6 +1107,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -1094,6 +1107,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
Match m = destructorPattern.Match(methodDef);
if (m.Success)
{
context.Step("Convert Finalize method to destructor", methodDef);
DestructorDeclaration dd = new DestructorDeclaration();
methodDef.Attributes.MoveTo(dd.Attributes);
dd.CopyAnnotationsFrom(methodDef);
@ -1103,6 +1117,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -1103,6 +1117,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// has an enclosing type at this point.
dd.Name = currentTypeDefinition!.Name;
methodDef.ReplaceWith(dd);
context.EndStep(dd);
return dd;
}
return null;
@ -1113,6 +1128,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -1113,6 +1128,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
Match m = destructorBodyPattern.Match(dtorDef.Body);
if (m.Success)
{
context.Step("Simplify destructor body", dtorDef);
dtorDef.Body = m.Get<BlockStatement>("body").Single().Detach();
return dtorDef;
}
@ -1139,6 +1155,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -1139,6 +1155,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
if (tryCatchFinallyPattern.IsMatch(tryFinally))
{
context.Step("Merge nested try-catch-finally", tryFinally);
TryCatchStatement tryCatch = (TryCatchStatement)tryFinally.TryBlock.Statements.Single();
tryFinally.TryBlock = tryCatch.TryBlock.Detach();
tryCatch.CatchClauses.MoveTo(tryFinally.CatchClauses);
@ -1171,6 +1188,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -1171,6 +1188,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
Match m = cascadingIfElsePattern.Match(node);
if (m.Success)
{
context.Step("Simplify cascading if-else", node);
IfElseStatement elseIf = m.Get<IfElseStatement>("nestedIfStatement").Single();
node.FalseStatement = elseIf.Detach();
}
@ -1191,6 +1209,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -1191,6 +1209,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
var bAndC = expr.Right as BinaryOperatorExpression;
if (bAndC != null && bAndC.Operator == expr.Operator)
{
context.Step("Reassociate conditional logic", expr);
// make bAndC the parent and expr the child.
// A conditional-and/or operator always has both operands present.
var b = bAndC.Left!.Detach();
@ -1199,6 +1218,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -1199,6 +1218,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
bAndC.Left = expr;
bAndC.Right = c;
expr.Right = b;
context.EndStep(bAndC);
return base.VisitBinaryOperatorExpression(bAndC);
}
break;
@ -1210,8 +1230,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -1210,8 +1230,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
if (expr.Operator == UnaryOperatorType.Not && expr.Expression is BinaryOperatorExpression { Operator: BinaryOperatorType.Equality } binary)
{
context.Step("Replace negated equality with inequality", expr);
binary.Operator = BinaryOperatorType.InEquality;
expr.ReplaceWith(binary.Detach());
context.EndStep(binary);
return VisitBinaryOperatorExpression(binary);
}
return base.VisitUnaryOperatorExpression(expr);
@ -1240,6 +1262,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -1240,6 +1262,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
Expression target = m.Get<Expression>("target").Single();
if (target.GetResolveResult().Type.IsReferenceType == false)
{
context.Step("Use pattern-based fixed statement", fixedStatement);
v.Initializer = target.Detach();
}
}
@ -1262,6 +1285,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -1262,6 +1285,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (!(usingStatement.ResourceAcquisition is VariableDeclarationStatement))
return usingStatement;
context.Step("Use enhanced using statement", usingStatement);
usingStatement.IsEnhanced = true;
return usingStatement;
}

11
ICSharpCode.Decompiler/CSharp/Transforms/PrettifyAssignments.cs

@ -63,9 +63,11 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -63,9 +63,11 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (CanConvertToCompoundAssignment(assignment.Left) && assignment.Left.IsMatch(binary.Left)
&& binary.Right != null && IsImplicitlyConvertible(binary.Right, expectedType))
{
assignment.Operator = GetAssignmentOperatorForBinaryOperator(binary.Operator);
if (assignment.Operator != AssignmentOperatorType.Assign)
var newOperator = GetAssignmentOperatorForBinaryOperator(binary.Operator);
if (newOperator != AssignmentOperatorType.Assign)
{
context.Step("Convert assignment to compound assignment", assignment);
assignment.Operator = newOperator;
// If we found a shorter operator, get rid of the BinaryOperatorExpression:
assignment.CopyAnnotationsFrom(binary);
assignment.Right = binary.Right;
@ -88,7 +90,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -88,7 +90,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
type = (assignment.Operator == AssignmentOperatorType.Add) ? UnaryOperatorType.PostIncrement : UnaryOperatorType.PostDecrement;
else
type = (assignment.Operator == AssignmentOperatorType.Add) ? UnaryOperatorType.Increment : UnaryOperatorType.Decrement;
assignment.ReplaceWith(new UnaryOperatorExpression(type, assignment.Left.Detach()).CopyAnnotationsFrom(assignment));
context.Step(type is UnaryOperatorType.Increment or UnaryOperatorType.PostIncrement ? "Convert assignment to increment" : "Convert assignment to decrement", assignment);
var unaryOperator = new UnaryOperatorExpression(type, assignment.Left.Detach()).CopyAnnotationsFrom(assignment);
assignment.ReplaceWith(unaryOperator);
context.EndStep(unaryOperator);
}
}
}

62
ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs

@ -75,6 +75,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -75,6 +75,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
bool isInExpressionTree = invocationExpression.Ancestors.OfType<LambdaExpression>().Any(
lambda => lambda.Annotation<IL.ILFunction>()?.Kind == IL.ILFunctionKind.ExpressionTree);
context.Step("Replace String.Concat with +", invocationExpression);
Expression arg0 = arguments[0].Detach();
Expression arg1 = arguments[1].Detach();
if (!isInExpressionTree)
@ -97,6 +98,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -97,6 +98,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
}
expr.CopyAnnotationsFrom(invocationExpression);
invocationExpression.ReplaceWith(expr);
context.EndStep(expr);
return;
}
@ -107,9 +109,11 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -107,9 +109,11 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
if (typeHandleOnTypeOfPattern.IsMatch(arguments[0]))
{
context.Step("Replace GetTypeFromHandle with typeof", invocationExpression);
Expression target = ((MemberReferenceExpression)arguments[0]).Target;
target.CopyInstructionsFrom(invocationExpression);
invocationExpression.ReplaceWith(target);
context.EndStep(target);
return;
}
}
@ -147,15 +151,20 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -147,15 +151,20 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
method.TypeArguments.Count == 1 &&
IsInstantiableTypeParameter(method.TypeArguments[0]))
{
invocationExpression.ReplaceWith(new ObjectCreateExpression(context.TypeSystemAstBuilder.ConvertType(method.TypeArguments.First())));
context.Step("Replace Activator.CreateInstance with new", invocationExpression);
var objectCreate = new ObjectCreateExpression(context.TypeSystemAstBuilder.ConvertType(method.TypeArguments.First()));
invocationExpression.ReplaceWith(objectCreate);
context.EndStep(objectCreate);
}
break;
case "System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray":
if (arguments.Length == 2 && context.Settings.Ranges)
{
context.Step("Replace RuntimeHelpers.GetSubArray with range indexer", invocationExpression);
var slicing = new IndexerExpression(arguments[0].Detach(), arguments[1].Detach());
slicing.CopyAnnotationsFrom(invocationExpression);
invocationExpression.ReplaceWith(slicing);
context.EndStep(slicing);
}
break;
}
@ -164,6 +173,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -164,6 +173,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
BinaryOperatorType? bop = GetBinaryOperatorTypeFromMetadataName(method.Name, out isChecked, context.Settings);
if (bop != null && arguments.Length == 2)
{
context.Step("Replace operator method with binary operator", invocationExpression);
invocationExpression.Arguments.Clear(); // detach arguments from invocationExpression
if (isChecked)
{
@ -173,13 +183,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -173,13 +183,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
invocationExpression.AddAnnotation(AddCheckedBlocks.UncheckedAnnotation);
}
invocationExpression.ReplaceWith(
new BinaryOperatorExpression(
arguments[0].UnwrapInDirectionExpression(),
bop.Value,
arguments[1].UnwrapInDirectionExpression()
).CopyAnnotationsFrom(invocationExpression)
);
var binaryOperator = new BinaryOperatorExpression(
arguments[0].UnwrapInDirectionExpression(),
bop.Value,
arguments[1].UnwrapInDirectionExpression()
).CopyAnnotationsFrom(invocationExpression);
invocationExpression.ReplaceWith(binaryOperator);
context.EndStep(binaryOperator);
return;
}
UnaryOperatorType? uop = GetUnaryOperatorTypeFromMetadataName(method.Name, out isChecked, context.Settings);
@ -199,28 +209,31 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -199,28 +209,31 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// because it doesn't assign the incremented value to a.
if (method.DeclaringType.IsKnownType(KnownTypeCode.Decimal))
{
context.Step("Replace decimal increment method with arithmetic", invocationExpression);
// Legacy csc optimizes "d + 1m" to "op_Increment(d)",
// so reverse that optimization here:
invocationExpression.ReplaceWith(
new BinaryOperatorExpression(
arguments[0].UnwrapInDirectionExpression().Detach(),
(uop == UnaryOperatorType.Increment ? BinaryOperatorType.Add : BinaryOperatorType.Subtract),
new PrimitiveExpression(1m)
).CopyAnnotationsFrom(invocationExpression)
);
var arithmetic = new BinaryOperatorExpression(
arguments[0].UnwrapInDirectionExpression().Detach(),
(uop == UnaryOperatorType.Increment ? BinaryOperatorType.Add : BinaryOperatorType.Subtract),
new PrimitiveExpression(1m)
).CopyAnnotationsFrom(invocationExpression);
invocationExpression.ReplaceWith(arithmetic);
context.EndStep(arithmetic);
}
}
else
{
context.Step("Replace operator method with unary operator", invocationExpression);
arguments[0].Remove(); // detach argument
invocationExpression.ReplaceWith(
new UnaryOperatorExpression(uop.Value, arguments[0].UnwrapInDirectionExpression()).CopyAnnotationsFrom(invocationExpression)
);
var unaryOperator = new UnaryOperatorExpression(uop.Value, arguments[0].UnwrapInDirectionExpression()).CopyAnnotationsFrom(invocationExpression);
invocationExpression.ReplaceWith(unaryOperator);
context.EndStep(unaryOperator);
}
return;
}
if (method.Name is "op_Explicit" or "op_CheckedExplicit" && arguments.Length == 1)
{
context.Step("Replace conversion operator method with cast", invocationExpression);
arguments[0].Remove(); // detach argument
if (method.Name == "op_CheckedExplicit")
{
@ -230,15 +243,18 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -230,15 +243,18 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{
invocationExpression.AddAnnotation(AddCheckedBlocks.UncheckedAnnotation);
}
invocationExpression.ReplaceWith(
new CastExpression(context.TypeSystemAstBuilder.ConvertType(method.ReturnType), arguments[0].UnwrapInDirectionExpression())
.CopyAnnotationsFrom(invocationExpression)
);
var cast = new CastExpression(context.TypeSystemAstBuilder.ConvertType(method.ReturnType), arguments[0].UnwrapInDirectionExpression())
.CopyAnnotationsFrom(invocationExpression);
invocationExpression.ReplaceWith(cast);
context.EndStep(cast);
return;
}
if (method.Name == "op_True" && arguments.Length == 1 && invocationExpression.Slot?.Kind == Slots.Condition)
{
invocationExpression.ReplaceWith(arguments[0].UnwrapInDirectionExpression());
context.Step("Remove op_True from condition", invocationExpression);
var condition = arguments[0].UnwrapInDirectionExpression();
invocationExpression.ReplaceWith(condition);
context.EndStep(condition);
return;
}

71
ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs

@ -19,9 +19,11 @@ @@ -19,9 +19,11 @@
#nullable enable
using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.TypeSystem;
namespace ICSharpCode.Decompiler.CSharp.Transforms
@ -36,6 +38,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -36,6 +38,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
public readonly TypeSystemAstBuilder TypeSystemAstBuilder;
public readonly DecompilerSettings Settings;
internal readonly DecompileRun DecompileRun;
public Stepper Stepper { get; set; }
readonly ITypeResolveContext decompilationContext;
@ -67,6 +70,74 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -67,6 +70,74 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
this.TypeSystemAstBuilder = typeSystemAstBuilder;
this.CancellationToken = decompileRun.CancellationToken;
this.Settings = decompileRun.Settings;
this.Stepper = new Stepper();
}
/// <summary>
/// Call this method immediately before performing a transform step.
/// Unlike <c>context.Stepper.Step()</c>, calls to this method are only compiled in debug builds.
/// </summary>
[Conditional("STEP")]
[DebuggerStepThrough]
internal void Step(string description, AstNode? near = null)
{
TrackModifiedNode(Stepper.Step(description, modifiedNode: near), near);
}
[Conditional("STEP")]
[DebuggerStepThrough]
internal void StepStartGroup(string description, AstNode? near = null)
{
TrackModifiedNode(Stepper.StartGroup(description, modifiedNode: near), near);
}
[Conditional("STEP")]
internal void StepEndGroup(bool keepIfEmpty = false)
{
Stepper.EndGroup(keepIfEmpty);
}
/// <summary>
/// Points the most recently recorded step at the node its mutation produced.
/// Call this after a <see cref="Step"/> whose modified node only comes into existence
/// during the mutation (e.g. the result of a ReplaceWith or a freshly inserted node).
/// </summary>
[Conditional("STEP")]
internal void EndStep(AstNode? modifiedNode)
{
if (Stepper.LastStep != null)
{
Stepper.LastStep.ModifiedNode = modifiedNode;
TrackModifiedNode(Stepper.LastStep, modifiedNode, insertFirst: true);
}
}
static void TrackModifiedNode(Stepper.Node step, AstNode? modifiedNode, bool insertFirst = false)
{
if (modifiedNode == null)
return;
AddCandidate(step, modifiedNode, insertFirst);
var marker = new DebugStepMarker();
modifiedNode.AddAnnotation(marker);
AddCandidate(step, marker, insertFirst: false);
for (var parent = modifiedNode.Parent; parent != null; parent = parent.Parent)
{
AddCandidate(step, parent, insertFirst: false);
}
}
static void AddCandidate(Stepper.Node step, object candidate, bool insertFirst)
{
if (step.ModifiedNodeCandidates.Contains(candidate))
return;
if (insertFirst)
step.ModifiedNodeCandidates.Insert(0, candidate);
else
step.ModifiedNodeCandidates.Add(candidate);
}
sealed class DebugStepMarker
{
}
}
}

30
ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs

@ -481,6 +481,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -481,6 +481,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
var ci = new ConstructorInitializer { ConstructorInitializerType = type };
context.Step("Move constructor call to initializer", stmt);
// Move arguments from invocation to initializer:
invocation.GetChildren(Slots.Argument).MoveTo(ci.Arguments);
// Add the initializer: (unless it is the default 'base()')
@ -489,6 +490,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -489,6 +490,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// Remove the statement
stmt.Remove();
context.EndStep(constructorDeclaration.Initializer);
return true;
}
@ -507,7 +509,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -507,7 +509,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// e.g. when a single static constructor is decompiled in isolation -- so the
// assignment must remain in the constructor body.
if (kind is InitializerKind.Primary)
{
context.Step("Remove redundant primary constructor assignment", stmt);
stmt.Remove();
}
continue;
}
@ -518,7 +523,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -518,7 +523,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
v = fd.Variables.Single();
if (v.Initializer is null)
{
v.Initializer = initializer.Detach();
context.Step("Move assignment to field initializer", stmt);
var movedInitializer = initializer.Detach();
v.Initializer = movedInitializer;
context.EndStep(movedInitializer);
}
else if (kind == InitializerKind.Static)
{
@ -548,7 +556,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -548,7 +556,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
Debug.Assert(pd.IsAutomaticProperty);
if (pd.Initializer is null)
{
pd.Initializer = initializer.Detach();
context.Step("Move assignment to property initializer", stmt);
var movedInitializer = initializer.Detach();
pd.Initializer = movedInitializer;
context.EndStep(movedInitializer);
}
else
{
@ -560,7 +571,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -560,7 +571,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
v = ev.Variables.Single();
if (v.Initializer is null)
{
v.Initializer = initializer.Detach();
context.Step("Move assignment to event initializer", stmt);
var movedInitializer = initializer.Detach();
v.Initializer = movedInitializer;
context.EndStep(movedInitializer);
}
else
{
@ -594,6 +608,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -594,6 +608,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (sequence.IsUnsafe && IntroduceUnsafeModifier.IsUnsafe(initializer))
{
context.Step("Add unsafe modifier to initialized member", declaringSyntaxNode);
declaringSyntaxNode.Modifiers |= Modifiers.Unsafe;
}
}
@ -626,6 +641,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -626,6 +641,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
var insertionPoint = (AstNode?)this.TypeDeclaration.TypeParameters.LastOrDefault() ?? this.TypeDeclaration.NameToken;
foreach (var param in PrimaryConstructorDecl.Parameters)
{
context.Step("Move primary constructor parameter to type", param);
param.Remove();
this.TypeDeclaration.InsertChildAfter(insertionPoint, param, Slots.Parameter);
insertionPoint = param;
@ -680,6 +696,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -680,6 +696,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (PrimaryConstructorDecl.HasModifier(Modifiers.Unsafe))
{
context.Step("Move unsafe modifier from primary constructor to type", this.TypeDeclaration);
this.TypeDeclaration.Modifiers |= Modifiers.Unsafe;
}
@ -689,11 +706,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -689,11 +706,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
var baseType = TypeDeclaration.BaseTypes.First();
var newBaseType = new InvocationAstType();
context.Step("Move primary constructor initializer to base type", baseType);
baseType.ReplaceWith(newBaseType);
newBaseType.BaseType = baseType;
initializer.Arguments.MoveTo(newBaseType.Arguments);
context.EndStep(newBaseType);
}
context.Step("Remove primary constructor body", PrimaryConstructorDecl);
PrimaryConstructorDecl.Remove();
}
@ -704,6 +724,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -704,6 +724,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (IsBeforeFieldInit && StaticConstructorDecl.Body is { Statements.Count: 0 })
{
context.Step("Remove empty static constructor", StaticConstructorDecl);
StaticConstructorDecl.Remove();
}
}
@ -733,7 +754,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -733,7 +754,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
bool retainBecauseOfDocumentation = context.Settings.ShowXmlDocumentation
&& context.DecompileRun.DocumentationProvider?.GetDocumentation(ctorMethod) != null;
if (!retainBecauseOfDocumentation)
{
context.Step("Remove implicit constructor", ctor);
ctor.Remove();
}
}
}

47
ICSharpCode.Decompiler/IL/Transforms/Stepper.cs

@ -54,6 +54,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -54,6 +54,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}
public IList<Node> Steps => steps;
public Node? LimitReachedStep { get; private set; }
public Node? LastStep { get; private set; }
public int StepLimit { get; set; } = int.MaxValue;
public bool IsDebug { get; set; }
@ -62,6 +64,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -62,6 +64,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{
public string Description { get; }
public ILInstruction? Position { get; set; }
public object? ModifiedNode { get; set; }
public IList<object> ModifiedNodeCandidates { get; } = new List<object>();
/// <summary>
/// BeginStep is inclusive.
/// </summary>
@ -96,39 +100,62 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -96,39 +100,62 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// May throw <see cref="StepLimitReachedException"/> in debug mode.
/// </summary>
[DebuggerStepThrough]
public void Step(string description, ILInstruction? near = null)
public Node Step(string description, ILInstruction? near = null, object? modifiedNode = null)
{
StepInternal(description, near);
return StepInternal(description, near, modifiedNode ?? near);
}
[DebuggerStepThrough]
private Node StepInternal(string description, ILInstruction? near)
private Node StepInternal(string description, ILInstruction? near, object? modifiedNode)
{
var stepNode = new Node($"{step}: {description}") {
Position = near,
ModifiedNode = modifiedNode,
BeginStep = step,
EndStep = step + 1
};
if (step == StepLimit)
{
LimitReachedStep = stepNode;
if (IsDebug)
Debugger.Break();
else
throw new StepLimitReachedException();
}
var stepNode = new Node($"{step}: {description}") {
Position = near,
BeginStep = step,
EndStep = step + 1
};
var p = groups.PeekOrDefault();
if (p != null)
p.Children.Add(stepNode);
else
steps.Add(stepNode);
LastStep = stepNode;
step++;
return stepNode;
}
[DebuggerStepThrough]
public void StartGroup(string description, ILInstruction? near = null)
public Node StartGroup(string description, ILInstruction? near = null, object? modifiedNode = null)
{
var stepNode = StepInternal(description, near, modifiedNode ?? near);
groups.Push(stepNode);
return stepNode;
}
public Node? GetStepByBeginStep(int beginStep)
{
groups.Push(StepInternal(description, near));
return FindStep(steps, beginStep);
static Node? FindStep(IEnumerable<Node> nodes, int beginStep)
{
foreach (var node in nodes)
{
if (node.BeginStep == beginStep)
return node;
var child = FindStep(node.Children, beginStep);
if (child != null)
return child;
}
return null;
}
}
public void EndGroup(bool keepIfEmpty = false)

93
ILSpy.Tests/Views/DebugStepsTests.cs

@ -18,6 +18,7 @@ @@ -18,6 +18,7 @@
#if DEBUG
using System;
using System.Linq;
using System.Threading.Tasks;
@ -27,10 +28,14 @@ using Avalonia.VisualTree; @@ -27,10 +28,14 @@ using Avalonia.VisualTree;
using AwesomeAssertions;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.ILSpy;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Docking;
using ICSharpCode.ILSpy.Languages;
using ICSharpCode.ILSpy.TextView;
using ICSharpCode.ILSpy.TreeNodes;
using ICSharpCode.ILSpy.ViewModels;
using ICSharpCode.ILSpy.Views;
@ -119,6 +124,75 @@ public class DebugStepsTests @@ -119,6 +124,75 @@ public class DebugStepsTests
"after switching to ILAst and decompiling, the VM's Steps must list the stepper's recorded transforms");
}
[AvaloniaTest]
public async Task CSharp_DebugSteps_Are_Grouped_By_Ast_Transform()
{
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3);
var languageService = AppComposition.Current.GetExport<LanguageService>();
var csharp = languageService.Languages.OfType<CSharpLanguage>().First();
languageService.CurrentLanguage = csharp;
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
typeNode.IsExpanded = true;
var method = typeNode.Children.OfType<MethodTreeNode>()
.First(m => m.MethodDefinition.Name == "Range");
vm.AssemblyTreeModel.SelectNode(method);
await vm.DockWorkspace.WaitForDecompiledTextAsync();
var debugStepsVm = AppComposition.Current.GetExport<DebugStepsPaneModel>();
await Waiters.WaitForAsync(
() => debugStepsVm.Steps?.Count > 0,
description: "DebugStepsPaneModel.Steps to be populated after the C# decompile");
var astTransformNames = CSharpDecompiler.GetAstTransforms()
.Select(transform => transform.GetType().Name)
.ToArray();
debugStepsVm.Steps!
.Select(step => StripStepNumber(step.Description))
.Should().Equal(astTransformNames,
"C# debug steps must use AST transforms as top-level groups");
var transformGroupWithChanges = debugStepsVm.Steps!
.FirstOrDefault(step => step.Children.Count > 0);
transformGroupWithChanges.Should().NotBeNull(
"individual C# AST mutation steps must be nested under their transform group");
transformGroupWithChanges!.Children
.Select(step => StripStepNumber(step.Description))
.Should().Contain(
description => !astTransformNames.Contains(description),
"nested C# debug steps must describe individual AST mutation points");
var collectedSteps = debugStepsVm.Steps;
var replayStep = transformGroupWithChanges.Children.First();
var tab = vm.DockWorkspace.ActiveDecompilerTab!;
tab.RestartDecompileWithStepLimit(replayStep.BeginStep, isDebug: false, replayStep.BeginStep);
tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
tab.Text.Should().NotBeNullOrWhiteSpace("C# replay before a selected AST mutation step must still emit code");
tab.DebugStepHighlight.Should().NotBeNull("C# replay before a selected AST mutation step must locate the changed node");
debugStepsVm.Steps.Should().BeSameAs(collectedSteps,
"a step-limited C# replay must not replace the full step tree shown by the pane");
tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep);
tab = await vm.DockWorkspace.WaitForDecompiledTextAsync();
tab.Text.Should().NotBeNullOrWhiteSpace("C# replay after a selected AST mutation step must still emit code");
tab.DebugStepHighlight.Should().NotBeNull("C# replay after a selected AST mutation step must locate the changed node");
debugStepsVm.Steps.Should().BeSameAs(collectedSteps,
"a step-limited C# replay must preserve the current full-run step tree and selection context");
static string StripStepNumber(string description)
{
var separatorIndex = description.IndexOf(": ", StringComparison.Ordinal);
return separatorIndex >= 0 ? description[(separatorIndex + 2)..] : description;
}
}
[AvaloniaTest]
public Task ILAst_And_TypedIL_Languages_Are_Registered_In_Debug_Builds()
{
@ -133,6 +207,25 @@ public class DebugStepsTests @@ -133,6 +207,25 @@ public class DebugStepsTests
languageService.Languages.Should().Contain(l => l.Name == "Typed IL");
return Task.CompletedTask;
}
[AvaloniaTest]
public Task NodeLookup_Resolves_Copied_Ast_Annotations()
{
var marker = new object();
var original = new IdentifierExpression("old");
original.AddAnnotation(marker);
var replacement = new IdentifierExpression("new").CopyAnnotationsFrom(original);
var lookup = new NodeLookup();
lookup.AddNode(replacement, 12, 3);
lookup.TryGetRange(marker, out var range).Should().BeTrue(
"C# debug-step markers copied by AST replacements must still resolve to emitted text");
range.Start.Should().Be(12);
range.Length.Should().Be(3);
return Task.CompletedTask;
}
[AvaloniaTest]
public Task Pane_Reports_Not_Available_For_Languages_Without_Debug_Steps()
{

7
ILSpy/DecompilationOptions.cs

@ -60,6 +60,13 @@ namespace ICSharpCode.ILSpy @@ -60,6 +60,13 @@ namespace ICSharpCode.ILSpy
/// </summary>
public int StepLimit { get; set; } = int.MaxValue;
/// <summary>
/// Step whose changed node should be highlighted after a debug-stepper re-decompile.
/// This can differ from <see cref="StepLimit"/> when showing the state after a step,
/// because the next recorded step is where the pipeline stops.
/// </summary>
public int? HighlightStep { get; set; }
/// <summary>
/// When true, transforms emit verbose debug information about their behaviour. Only
/// meaningful in combination with <see cref="StepLimit"/> — the Debug Steps pane sets

9
ILSpy/Languages/CSharpHighlightingTokenWriter.cs

@ -124,6 +124,12 @@ namespace ICSharpCode.ILSpy.Languages @@ -124,6 +124,12 @@ namespace ICSharpCode.ILSpy.Languages
//this.externAliasKeywordColor = ...;
}
public CSharpHighlightingTokenWriter(TokenWriter decoratedWriter, AvaloniaEditTextOutput textOutput, ILocatable? locatable = null)
: this(decoratedWriter, (ISmartTextOutput?)textOutput, locatable)
{
this.nodeTrackingOutput = textOutput;
}
public override void WriteKeyword(string keyword)
{
HighlightingColor? color = null;
@ -496,6 +502,7 @@ namespace ICSharpCode.ILSpy.Languages @@ -496,6 +502,7 @@ namespace ICSharpCode.ILSpy.Languages
public override void StartNode(AstNode node)
{
nodeTrackingOutput?.MarkNodeStart(node);
nodeStack.Push(node);
base.StartNode(node);
}
@ -503,6 +510,7 @@ namespace ICSharpCode.ILSpy.Languages @@ -503,6 +510,7 @@ namespace ICSharpCode.ILSpy.Languages
public override void EndNode(AstNode node)
{
base.EndNode(node);
nodeTrackingOutput?.MarkNodeEnd(node);
nodeStack.Pop();
}
@ -511,6 +519,7 @@ namespace ICSharpCode.ILSpy.Languages @@ -511,6 +519,7 @@ namespace ICSharpCode.ILSpy.Languages
int currentColorBegin = -1;
readonly ILocatable? locatable;
readonly ISmartTextOutput? textOutput;
readonly AvaloniaEditTextOutput? nodeTrackingOutput;
// Wraps a base WriteX call so its output lands inside a highlighting span for the given colour
// (or no span when null) -- replacing the begin/end guard each WriteX override used to repeat.

29
ILSpy/Languages/CSharpLanguage.DebugSteps.cs

@ -32,18 +32,16 @@ using ICSharpCode.ILSpy.ViewModels; @@ -32,18 +32,16 @@ using ICSharpCode.ILSpy.ViewModels;
namespace ICSharpCode.ILSpy.Languages
{
/// <summary>
/// Debug Steps support for the C# language: coarse, one step per AST transform, shown in the
/// Debug Steps pane like the ILAst language already does for IL transforms. The step list is
/// static -- the C# AST pipeline (<see cref="CSharpDecompiler.GetAstTransforms"/>) is the same
/// for every member -- so a selected step's index maps straight onto <see
/// cref="DecompilationOptions.StepLimit"/>, which CreateDecompiler turns into the number of AST
/// transforms to keep before re-rendering.
/// Debug Steps support for the C# language, shown in the Debug Steps pane like the ILAst
/// language already does for IL transforms. A full decompile records AST transform groups with
/// individual mutation steps inside; a selected step's index is replayed by re-decompiling with <see
/// cref="DecompilationOptions.StepLimit"/>.
/// </summary>
partial class CSharpLanguage : IDebugStepProvider
{
Stepper? stepper;
Stepper stepper = new Stepper();
public Stepper Stepper => stepper ??= BuildStepper();
public Stepper Stepper => stepper;
public event EventHandler? StepperUpdated;
@ -51,18 +49,7 @@ namespace ICSharpCode.ILSpy.Languages @@ -51,18 +49,7 @@ namespace ICSharpCode.ILSpy.Languages
// tree for C#, unlike ILAst's writing-options checkboxes.
public object? StepOptions => null;
// One node per AST transform, in pipeline order. Stepper.Step assigns BeginStep=i /
// EndStep=i+1 automatically, so "show state before step i" re-decompiles with StepLimit=i
// (run i transforms) and "after" with StepLimit=i+1 -- exactly the cap CreateDecompiler wants.
static Stepper BuildStepper()
{
var s = new Stepper();
foreach (var transform in CSharpDecompiler.GetAstTransforms())
s.Step(transform.GetType().Name);
return s;
}
partial void OnCSharpDecompiled(ITextOutput output, DecompilationOptions options)
partial void OnCSharpDecompiled(CSharpDecompiler decompiler, ITextOutput output, DecompilationOptions options)
{
// The button always shows so the pane is one click away; mirrors the ILAst language.
// DockWorkspace is resolved lazily (an ImportingConstructor import would form a MEF
@ -74,7 +61,7 @@ namespace ICSharpCode.ILSpy.Languages @@ -74,7 +61,7 @@ namespace ICSharpCode.ILSpy.Languages
// pane itself) must leave the tree and the user's selection intact.
if (options.StepLimit == int.MaxValue)
{
_ = Stepper;
stepper = decompiler.Stepper;
StepperUpdated?.Invoke(this, EventArgs.Empty);
}
}

89
ILSpy/Languages/CSharpLanguage.cs

@ -33,6 +33,7 @@ using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; @@ -33,6 +33,7 @@ using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.CSharp.Transforms;
using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.Output;
using ICSharpCode.Decompiler.Solution;
@ -313,11 +314,8 @@ namespace ICSharpCode.ILSpy.Languages @@ -313,11 +314,8 @@ namespace ICSharpCode.ILSpy.Languages
CancellationToken = options.CancellationToken,
DebugInfoProvider = module.GetDebugInfoOrNull(),
};
// The Debug Steps pane stops the AST pipeline at a chosen step by re-decompiling with
// options.StepLimit = number of AST transforms to keep; pop the rest from the end.
// StepLimit is int.MaxValue for a normal decompile, so nothing is removed.
while (decompiler.AstTransforms.Count > options.StepLimit)
decompiler.AstTransforms.RemoveAt(decompiler.AstTransforms.Count - 1);
decompiler.Stepper.StepLimit = options.StepLimit;
decompiler.Stepper.IsDebug = options.IsDebug;
if (options.EscapeInvalidIdentifiers)
decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers());
return decompiler;
@ -345,25 +343,25 @@ namespace ICSharpCode.ILSpy.Languages @@ -345,25 +343,25 @@ namespace ICSharpCode.ILSpy.Languages
{
var members = CollectFieldsAndCtors(methodDefinition.DeclaringTypeDefinition!, methodDefinition.IsStatic);
decompiler.AstTransforms.Add(new SelectCtorTransform(methodDefinition));
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(members));
WriteCode(output, options, decompiler.Decompile(members), decompiler);
}
else
{
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(method.MetadataToken));
WriteCode(output, options, decompiler.Decompile(method.MetadataToken), decompiler);
}
OnCSharpDecompiled(output, options);
OnCSharpDecompiled(decompiler, output, options);
}
// Implemented only under DEBUG (CSharpLanguage.DebugSteps.cs) to feed the Debug Steps pane;
// a no-op partial in Release.
partial void OnCSharpDecompiled(ITextOutput output, DecompilationOptions options);
partial void OnCSharpDecompiled(CSharpDecompiler decompiler, ITextOutput output, DecompilationOptions options);
public override void DecompileProperty(IProperty property, ITextOutput output, DecompilationOptions options)
{
CSharpDecompiler decompiler = BeginDecompile(property, output, options);
WriteCommentLine(output, TypeToString(property.DeclaringType));
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(property.MetadataToken));
OnCSharpDecompiled(output, options);
WriteCode(output, options, decompiler.Decompile(property.MetadataToken), decompiler);
OnCSharpDecompiled(decompiler, output, options);
}
public override void DecompileField(IField field, ITextOutput output, DecompilationOptions options)
@ -372,16 +370,16 @@ namespace ICSharpCode.ILSpy.Languages @@ -372,16 +370,16 @@ namespace ICSharpCode.ILSpy.Languages
WriteCommentLine(output, TypeToString(field.DeclaringType));
if (field.IsConst)
{
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(field.MetadataToken));
WriteCode(output, options, decompiler.Decompile(field.MetadataToken), decompiler);
}
else
{
var members = CollectFieldsAndCtors(field.DeclaringTypeDefinition!, field.IsStatic);
var resolvedField = decompiler.TypeSystem.MainModule.GetDefinition((FieldDefinitionHandle)field.MetadataToken);
decompiler.AstTransforms.Add(new SelectFieldTransform(resolvedField));
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(members));
WriteCode(output, options, decompiler.Decompile(members), decompiler);
}
OnCSharpDecompiled(output, options);
OnCSharpDecompiled(decompiler, output, options);
}
/// <summary>
@ -408,24 +406,24 @@ namespace ICSharpCode.ILSpy.Languages @@ -408,24 +406,24 @@ namespace ICSharpCode.ILSpy.Languages
CSharpDecompiler decompiler = BeginDecompile(extension, output, options);
WriteCommentLine(output, TypeToString(commentType,
ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.SupportExtensionDeclarations));
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.DecompileExtension(extension.MetadataToken));
OnCSharpDecompiled(output, options);
WriteCode(output, options, decompiler.DecompileExtension(extension.MetadataToken), decompiler);
OnCSharpDecompiled(decompiler, output, options);
}
public override void DecompileEvent(IEvent ev, ITextOutput output, DecompilationOptions options)
{
CSharpDecompiler decompiler = BeginDecompile(ev, output, options);
WriteCommentLine(output, TypeToString(ev.DeclaringType));
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(ev.MetadataToken));
OnCSharpDecompiled(output, options);
WriteCode(output, options, decompiler.Decompile(ev.MetadataToken), decompiler);
OnCSharpDecompiled(decompiler, output, options);
}
public override void DecompileType(ITypeDefinition type, ITextOutput output, DecompilationOptions options)
{
CSharpDecompiler decompiler = BeginDecompile(type, output, options);
WriteCommentLine(output, TypeToString(type, ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames));
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(type.MetadataToken));
OnCSharpDecompiled(output, options);
WriteCode(output, options, decompiler.Decompile(type.MetadataToken), decompiler);
OnCSharpDecompiled(decompiler, output, options);
}
public override ProjectId? DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options)
@ -511,7 +509,7 @@ namespace ICSharpCode.ILSpy.Languages @@ -511,7 +509,7 @@ namespace ICSharpCode.ILSpy.Languages
SyntaxTree st = options.FullDecompilation
? decompiler.DecompileWholeModuleAsSingleFile()
: decompiler.DecompileModuleAndAssemblyAttributes();
WriteCode(output, options.DecompilerSettings, decompiler, st);
WriteCode(output, options, st, decompiler);
return null;
}
@ -709,13 +707,15 @@ namespace ICSharpCode.ILSpy.Languages @@ -709,13 +707,15 @@ namespace ICSharpCode.ILSpy.Languages
}
}
static void WriteCode(ITextOutput output, DecompilerSettings settings, CSharpDecompiler decompiler, SyntaxTree syntaxTree)
static void WriteCode(ITextOutput output, DecompilationOptions options, SyntaxTree syntaxTree, CSharpDecompiler decompiler)
{
var settings = options.DecompilerSettings;
syntaxTree.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true });
output.IndentationString = settings.CSharpFormattingOptions.IndentationString;
TokenWriter tokenWriter = new TextTokenWriter(output, settings, decompiler.TypeSystem);
if (output is TextView.ISmartTextOutput smartOutput)
if (output is TextView.AvaloniaEditTextOutput avaloniaOutput)
tokenWriter = new CSharpHighlightingTokenWriter(tokenWriter, avaloniaOutput);
else if (output is TextView.ISmartTextOutput smartOutput)
tokenWriter = new CSharpHighlightingTokenWriter(tokenWriter, smartOutput);
// For the on-screen C# view, harvest the IL-offset/line map for body bookmarks during this
@ -728,6 +728,47 @@ namespace ICSharpCode.ILSpy.Languages @@ -728,6 +728,47 @@ namespace ICSharpCode.ILSpy.Languages
syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions));
bookmarkCollector?.Publish();
if (output is TextView.AvaloniaEditTextOutput nodeOutput
&& TryGetDebugStepHighlightRange(decompiler, options, nodeOutput.NodeLookup, out var range))
{
nodeOutput.DebugStepHighlight = range;
}
}
static bool TryGetDebugStepHighlightRange(CSharpDecompiler decompiler, DecompilationOptions options, TextView.NodeLookup nodeLookup, out TextView.TextRange range)
{
range = default;
if (options.StepLimit == int.MaxValue)
return false;
if (options.HighlightStep is { } highlightStep)
{
if (TryGetRange(decompiler.Stepper.GetStepByBeginStep(highlightStep), nodeLookup, out range))
return true;
if (decompiler.Stepper.LimitReachedStep is { BeginStep: var limitStep } reachedStep
&& limitStep == highlightStep)
{
return TryGetRange(reachedStep, nodeLookup, out range);
}
return false;
}
if (TryGetRange(decompiler.Stepper.LimitReachedStep, nodeLookup, out range))
return true;
if (options.StepLimit > 0)
return TryGetRange(decompiler.Stepper.GetStepByBeginStep(options.StepLimit - 1), nodeLookup, out range);
return false;
static bool TryGetRange(Stepper.Node? step, TextView.NodeLookup nodeLookup, out TextView.TextRange range)
{
range = default;
if (step == null)
return false;
foreach (var candidate in step.ModifiedNodeCandidates)
{
if (nodeLookup.TryGetRange(candidate, out range))
return true;
}
return step.ModifiedNode != null && nodeLookup.TryGetRange(step.ModifiedNode, out range);
}
}
void AddWarningMessage(MetadataFile module, ITextOutput output, string line1, string? line2 = null,

20
ILSpy/TextView/AvaloniaEditTextOutput.cs

@ -54,6 +54,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -54,6 +54,7 @@ namespace ICSharpCode.ILSpy.TextView
public int LengthLimit { get; set; } = int.MaxValue;
readonly Stack<(int Offset, HighlightingColor Color)> openSpans = new();
readonly Stack<(object Node, int Offset)> openNodes = new();
readonly Stack<(NewFolding Folding, int StartLine)> openFoldings = new();
readonly List<NewFolding> foldings = new();
int indent;
@ -87,6 +88,10 @@ namespace ICSharpCode.ILSpy.TextView @@ -87,6 +88,10 @@ namespace ICSharpCode.ILSpy.TextView
/// <summary>Maps reference targets to their definition offsets in the rendered text.</summary>
public DefinitionLookup DefinitionLookup { get; } = new();
internal NodeLookup NodeLookup { get; } = new();
internal TextRange? DebugStepHighlight { get; set; }
readonly List<Bookmarks.MethodDebugInfo> methodDebugInfos = new();
/// <summary>
@ -300,5 +305,20 @@ namespace ICSharpCode.ILSpy.TextView @@ -300,5 +305,20 @@ namespace ICSharpCode.ILSpy.TextView
highlightingSpans.Add((start, length, color));
}
}
internal void MarkNodeStart(object node)
{
openNodes.Push((node, builder.Length));
}
internal void MarkNodeEnd(object node)
{
if (openNodes.Count == 0)
return;
var (currentNode, start) = openNodes.Pop();
if (!ReferenceEquals(currentNode, node))
return;
NodeLookup.AddNode(node, start, builder.Length - start);
}
}
}

12
ILSpy/TextView/DecompilerTabPageModel.cs

@ -183,6 +183,9 @@ namespace ICSharpCode.ILSpy.TextView @@ -183,6 +183,9 @@ namespace ICSharpCode.ILSpy.TextView
[ObservableProperty]
private DefinitionLookup? definitionLookup;
[ObservableProperty]
private TextRange? debugStepHighlight;
/// <summary>
/// IL-offset &lt;-&gt; line maps for the methods in this document (C# only). Lets bookmarks
/// anchor in-method lines by IL offset and the gutter place their icons. Null for non-C#
@ -379,6 +382,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -379,6 +382,7 @@ namespace ICSharpCode.ILSpy.TextView
// every run; set non-default by RestartDecompileWithStepLimit before kicking off a
// debug-stepper decompile. Set on the UI thread only — no inter-thread access.
int pendingStepLimit = int.MaxValue;
int? pendingHighlightStep;
bool pendingIsDebug;
/// <summary>
@ -397,9 +401,10 @@ namespace ICSharpCode.ILSpy.TextView @@ -397,9 +401,10 @@ namespace ICSharpCode.ILSpy.TextView
/// is <see cref="int.MaxValue"/>). <paramref name="isDebug"/> toggles the transforms'
/// verbose-debug emission. No-op when there's nothing currently being decompiled.
/// </summary>
public void RestartDecompileWithStepLimit(int stepLimit, bool isDebug)
public void RestartDecompileWithStepLimit(int stepLimit, bool isDebug, int? highlightStep = null)
{
pendingStepLimit = stepLimit;
pendingHighlightStep = highlightStep;
pendingIsDebug = isDebug;
StartDecompile();
}
@ -511,6 +516,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -511,6 +516,7 @@ namespace ICSharpCode.ILSpy.TextView
References = null;
DefinitionLookup = null;
DebugInfo = null;
DebugStepHighlight = null;
UIElements = null;
Text = string.Empty;
IsDecompiling = false;
@ -537,8 +543,10 @@ namespace ICSharpCode.ILSpy.TextView @@ -537,8 +543,10 @@ namespace ICSharpCode.ILSpy.TextView
// stable value, and reset the fields to defaults so the NEXT decompile runs
// at full fidelity unless RestartDecompileWithStepLimit sets them again.
var stepLimit = pendingStepLimit;
var highlightStep = pendingHighlightStep;
var isDebug = pendingIsDebug;
pendingStepLimit = int.MaxValue;
pendingHighlightStep = null;
pendingIsDebug = false;
var outputLengthLimit = pendingOutputLengthLimit;
pendingOutputLengthLimit = DefaultOutputLengthLimit;
@ -553,6 +561,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -553,6 +561,7 @@ namespace ICSharpCode.ILSpy.TextView
decompilerSettings ?? new ICSharpCode.Decompiler.DecompilerSettings()) {
CancellationToken = cts.Token,
StepLimit = stepLimit,
HighlightStep = highlightStep,
IsDebug = isDebug,
};
try
@ -745,6 +754,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -745,6 +754,7 @@ namespace ICSharpCode.ILSpy.TextView
: output.MethodDebugInfos.Count > 0
? new Bookmarks.DecompiledDebugInfo(output.MethodDebugInfos)
: Bookmarks.DecompiledDebugInfo.Empty;
DebugStepHighlight = output.DebugStepHighlight;
UIElements = output.UIElements;
Text = text;
}

30
ILSpy/TextView/DecompilerTextView.axaml.cs

@ -64,6 +64,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -64,6 +64,7 @@ namespace ICSharpCode.ILSpy.TextView
// softer green for the actual definition.
static readonly Color LocalMatchBackground = Colors.GreenYellow;
static readonly Color LocalDefinitionBackground = Color.FromArgb(0x80, 0xA0, 0xFF, 0xA0);
static readonly Color DebugStepBackground = Color.FromArgb(0x80, 0xFF, 0xD7, 0x66);
// Stay-open corridor for the rich popup: as long as the pointer is closer than
// `distanceToPopupLimit` to the popup edges, the popup stays. The limit shrinks toward
@ -78,6 +79,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -78,6 +79,7 @@ namespace ICSharpCode.ILSpy.TextView
TextMarkerService textMarkerService = null!;
BracketHighlightRenderer bracketHighlightRenderer = null!;
readonly List<TextMarker> localReferenceMarks = new();
readonly List<TextMarker> debugStepMarks = new();
readonly List<AvaloniaEdit.Rendering.VisualLineElementGenerator> activeCustomGenerators = new();
RichTextColorizer? activeColorizer;
FoldingManager? activeFoldingManager;
@ -1043,6 +1045,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -1043,6 +1045,7 @@ namespace ICSharpCode.ILSpy.TextView
// force-close even if the popup currently wants to stay (mouseClick: true).
TryCloseExistingPopup(mouseClick: true);
ClearLocalReferenceMarks();
ClearDebugStepMarks();
Editor.SyntaxHighlighting = HighlightingService.GetByExtension(model.SyntaxExtension);
Editor.Document.Text = model.Text;
@ -1072,6 +1075,8 @@ namespace ICSharpCode.ILSpy.TextView @@ -1072,6 +1075,8 @@ namespace ICSharpCode.ILSpy.TextView
if (restoreViewState)
RestoreOrResetViewState(pendingState);
ApplyDebugStepHighlight(model.DebugStepHighlight);
// Position at a navigated-to bookmark once its document (and debug map) has landed. Only on
// the final content (the Text change, where restoreViewState is set), AFTER the view-state
// reset above, so the bookmark scroll is the last word on position and the highlight plays
@ -1246,6 +1251,31 @@ namespace ICSharpCode.ILSpy.TextView @@ -1246,6 +1251,31 @@ namespace ICSharpCode.ILSpy.TextView
localReferenceMarks.Clear();
}
void ApplyDebugStepHighlight(TextRange? range)
{
ClearDebugStepMarks();
if (range is not { } r || r.Length <= 0)
return;
var start = Math.Clamp(r.Start, 0, Editor.Document.TextLength);
var end = Math.Clamp(r.Start + r.Length, start, Editor.Document.TextLength);
if (end <= start)
return;
var mark = textMarkerService.Create(start, end - start);
mark.BackgroundColor = DebugStepBackground;
debugStepMarks.Add(mark);
Editor.TextArea.Caret.Offset = start;
Editor.TextArea.Caret.BringCaretToView();
CaretHighlightAdorner.DisplayCaretHighlightAnimation(Editor.TextArea);
}
void ClearDebugStepMarks()
{
foreach (var mark in debugStepMarks)
textMarkerService.Remove(mark);
debugStepMarks.Clear();
}
// Live cursor + full ScrollOffset, queried at hover-event time. Returns null if the
// pointer is past the end of the line so we don't snap back to the last visual
// column when the user is hovering empty trailing space.

50
ILSpy/TextView/NodeLookup.cs

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// 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.Generic;
using ICSharpCode.Decompiler.CSharp.Syntax;
namespace ICSharpCode.ILSpy.TextView
{
/// <summary>
/// Maps syntax tree nodes to character ranges in the rendered text.
/// </summary>
internal sealed class NodeLookup
{
readonly Dictionary<object, TextRange> nodes = new(ReferenceEqualityComparer.Instance);
public bool TryGetRange(object node, out TextRange range)
=> nodes.TryGetValue(node, out range);
public void AddNode(object node, int start, int length)
{
if (length > 0)
{
nodes[node] = new TextRange(start, length);
if (node is IAnnotatable annotatable)
{
foreach (var annotation in annotatable.Annotations)
{
nodes[annotation] = new TextRange(start, length);
}
}
}
}
}
}

22
ILSpy/TextView/TextRange.cs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// 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.
namespace ICSharpCode.ILSpy.TextView
{
public readonly record struct TextRange(int Start, int Length);
}

10
ILSpy/ViewModels/DebugStepsPaneModel.cs

@ -109,8 +109,8 @@ namespace ICSharpCode.ILSpy.ViewModels @@ -109,8 +109,8 @@ namespace ICSharpCode.ILSpy.ViewModels
{
Id = PaneContentId;
Title = "Debug Steps";
ShowStateBeforeCommand = new RelayCommand(() => RequestRedecompile(SelectedStep?.BeginStep ?? int.MaxValue, isDebug: false));
ShowStateAfterCommand = new RelayCommand(() => RequestRedecompile(SelectedStep?.EndStep ?? int.MaxValue, isDebug: false));
ShowStateBeforeCommand = new RelayCommand(() => RequestRedecompile(SelectedStep?.BeginStep ?? int.MaxValue, isDebug: false, SelectedStep?.BeginStep));
ShowStateAfterCommand = new RelayCommand(() => RequestRedecompile(SelectedStep?.EndStep ?? int.MaxValue, isDebug: false, SelectedStep?.BeginStep));
DebugStepCommand = new RelayCommand(() => {
// "Debug this step" relies on Stepper.Step calling Debugger.Break() when
// step == StepLimit — which is a silent no-op without a debugger attached.
@ -123,7 +123,7 @@ namespace ICSharpCode.ILSpy.ViewModels @@ -123,7 +123,7 @@ namespace ICSharpCode.ILSpy.ViewModels
if (!System.Diagnostics.Debugger.Launch())
AppEnv.AppLog.Mark("DebugStep: Debugger.Launch returned false; the upcoming Stepper.Step break is a no-op without a debugger attached.");
}
RequestRedecompile(SelectedStep?.BeginStep ?? int.MaxValue, isDebug: true);
RequestRedecompile(SelectedStep?.BeginStep ?? int.MaxValue, isDebug: true, SelectedStep?.BeginStep);
});
}
@ -222,12 +222,12 @@ namespace ICSharpCode.ILSpy.ViewModels @@ -222,12 +222,12 @@ namespace ICSharpCode.ILSpy.ViewModels
RequestRedecompile(lastSelectedStep, isDebug: false);
}
void RequestRedecompile(int stepLimit, bool isDebug)
void RequestRedecompile(int stepLimit, bool isDebug, int? highlightStep = null)
{
lastSelectedStep = stepLimit;
// Composition unavailable in design-time previews; the gesture is a no-op there.
var dock = AppComposition.TryGetExport<DockWorkspace>();
dock?.ActiveDecompilerTab?.RestartDecompileWithStepLimit(stepLimit, isDebug);
dock?.ActiveDecompilerTab?.RestartDecompileWithStepLimit(stepLimit, isDebug, highlightStep);
}
}
}

Loading…
Cancel
Save