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. 14
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  3. 33
      ICSharpCode.Decompiler/CSharp/Transforms/AddCheckedBlocks.cs
  4. 1
      ICSharpCode.Decompiler/CSharp/Transforms/AddXmlDocumentationTransform.cs
  5. 18
      ICSharpCode.Decompiler/CSharp/Transforms/CombineQueryExpressions.cs
  6. 45
      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. 20
      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. 50
      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
func(); func();
func2(); 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);
}
} }
} }

14
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

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

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

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

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

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

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

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

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

@ -213,6 +213,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{ {
if (stmt.Expression is DirectionExpression dir && IsValidInStatementExpression(dir.Expression)) if (stmt.Expression is DirectionExpression dir && IsValidInStatementExpression(dir.Expression))
{ {
context.Step("Unwrap direction expression statement", stmt);
stmt.Expression = dir.Expression.Detach(); stmt.Expression = dir.Expression.Detach();
} }
else if (!IsValidInStatementExpression(stmt.Expression)) else if (!IsValidInStatementExpression(stmt.Expression))
@ -222,12 +223,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// if possible use C# 7.0 discard-assignment // if possible use C# 7.0 discard-assignment
if (context.Settings.Discards && !ExpressionBuilder.HidesVariableWithName(function, "_")) if (context.Settings.Discards && !ExpressionBuilder.HidesVariableWithName(function, "_"))
{ {
context.Step("Assign invalid expression statement to discard", stmt);
stmt.Expression = new AssignmentExpression( stmt.Expression = new AssignmentExpression(
new IdentifierExpression("_"), // no ResolveResult new IdentifierExpression("_"), // no ResolveResult
stmt.Expression.Detach()); stmt.Expression.Detach());
} }
else else
{ {
context.Step("Assign invalid expression statement to temporary", stmt);
// assign result to dummy variable // assign result to dummy variable
var type = stmt.Expression.GetResolveResult().Type; var type = stmt.Expression.GetResolveResult().Type;
var v = function.RegisterVariable( var v = function.RegisterVariable(
@ -542,7 +545,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
continue; continue;
var designation = StatementBuilder.TranslateDeconstructionDesignation(deconstruct, isForeach: false); 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) foreach (var v in usedVariables)
{ {
@ -595,7 +601,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
void InsertVariableDeclarations(TransformContext context) 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) foreach (var (ilVariable, v) in variableDict)
{ {
if (v.RemovedDueToCollision || v.DeclaredInDeconstruction) if (v.RemovedDueToCollision || v.DeclaredInDeconstruction)
@ -621,6 +627,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{ {
type.AddTrailingTrivia(new Comment("pinned", CommentType.MultiLine)); type.AddTrailingTrivia(new Comment("pinned", CommentType.MultiLine));
} }
replacements.Add((v.InsertionPoint.nextNode, () => {
var vds = new VariableDeclarationStatement(type, v.Name, assignment.Right.Detach()); var vds = new VariableDeclarationStatement(type, v.Name, assignment.Right.Detach());
var init = vds.Variables.Single(); var init = vds.Variables.Single();
init.AddAnnotation(assignment.Left.GetResolveResult()); init.AddAnnotation(assignment.Left.GetResolveResult());
@ -631,7 +638,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
init.AddAnnotation(annotation); init.AddAnnotation(annotation);
} }
} }
replacements.Add((v.InsertionPoint.nextNode, vds)); return vds;
}, "Combine variable declaration with initializer"));
} }
else if (CanBeDeclaredAsOutVariable(v, out var dirExpr)) else if (CanBeDeclaredAsOutVariable(v, out var dirExpr))
{ {
@ -675,7 +683,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
ovd.RemoveAnnotations<ResolveResult>(); ovd.RemoveAnnotations<ResolveResult>();
ovd.AddAnnotation(new OutVarResolveResult(v.Type)); ovd.AddAnnotation(new OutVarResolveResult(v.Type));
} }
replacements.Add((dirExpr, ovd)); replacements.Add((dirExpr, () => ovd, "Declare out variable"));
} }
else else
{ {
@ -688,6 +696,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
} }
var vds = new VariableDeclarationStatement(type, v.Name, initializer); var vds = new VariableDeclarationStatement(type, v.Name, initializer);
vds.Variables.Single().AddAnnotation(new ILVariableResolveResult(ilVariable)); vds.Variables.Single().AddAnnotation(new ILVariableResolveResult(ilVariable));
context.Step("Insert variable declaration", v.InsertionPoint.nextNode);
if (v.InsertionPoint.nextNode.Parent is LambdaExpression lambda) if (v.InsertionPoint.nextNode.Parent is LambdaExpression lambda)
{ {
Debug.Assert(lambda.Body is not BlockStatement); Debug.Assert(lambda.Body is not BlockStatement);
@ -708,13 +717,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{ {
AstType unsafeType = context.TypeSystemAstBuilder.ConvertType( AstType unsafeType = context.TypeSystemAstBuilder.ConvertType(
context.TypeSystem.FindType(KnownTypeCode.Unsafe)); context.TypeSystem.FindType(KnownTypeCode.Unsafe));
AstNode insertedNode;
if (context.Settings.OutVariables) if (context.Settings.OutVariables)
{ {
var outVarDecl = new OutVarDeclarationExpression(type.Clone(), v.Name); var outVarDecl = new OutVarDeclarationExpression(type.Clone(), v.Name);
outVarDecl.Variable.AddAnnotation(new ILVariableResolveResult(ilVariable)); outVarDecl.Variable.AddAnnotation(new ILVariableResolveResult(ilVariable));
insertionParent.InsertChildBefore( var skipInitStatement = new ExpressionStatement {
v.InsertionPoint.nextNode,
new ExpressionStatement {
Expression = new InvocationExpression { Expression = new InvocationExpression {
Target = new MemberReferenceExpression { Target = new MemberReferenceExpression {
Target = new TypeReferenceExpression(unsafeType), Target = new TypeReferenceExpression(unsafeType),
@ -724,8 +732,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
outVarDecl outVarDecl
} }
} }
}, };
insertionParent.InsertChildBefore(
v.InsertionPoint.nextNode,
skipInitStatement,
Slots.Statement); Slots.Statement);
insertedNode = skipInitStatement;
} }
else else
{ {
@ -733,9 +745,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
v.InsertionPoint.nextNode, v.InsertionPoint.nextNode,
vds, vds,
Slots.Statement); Slots.Statement);
insertionParent.InsertChildBefore( insertedNode = vds;
v.InsertionPoint.nextNode, var skipInitStatement = new ExpressionStatement {
new ExpressionStatement {
Expression = new InvocationExpression { Expression = new InvocationExpression {
Target = new MemberReferenceExpression { Target = new MemberReferenceExpression {
Target = new TypeReferenceExpression(unsafeType), Target = new TypeReferenceExpression(unsafeType),
@ -749,9 +760,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
) )
} }
} }
}, };
insertionParent.InsertChildBefore(
v.InsertionPoint.nextNode,
skipInitStatement,
Slots.Statement); Slots.Statement);
} }
context.EndStep(insertedNode);
} }
else else
{ {
@ -759,13 +774,17 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
v.InsertionPoint.nextNode, v.InsertionPoint.nextNode,
vds, vds,
Slots.Statement); 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 // 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); oldNode.ReplaceWith(newNode);
context.EndStep(newNode);
} }
} }

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

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

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

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

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

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

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

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

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

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

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

@ -26,6 +26,9 @@ using System.Linq;
using ICSharpCode.Decompiler.CSharp.Resolver; using ICSharpCode.Decompiler.CSharp.Resolver;
using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Syntax;
#if STEP
using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching;
#endif
using ICSharpCode.Decompiler.CSharp.TypeSystem; using ICSharpCode.Decompiler.CSharp.TypeSystem;
using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.Semantics; using ICSharpCode.Decompiler.Semantics;
@ -73,7 +76,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{ {
resolvedNamespaces.Add(resolvedNamespace); 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
{ {
readonly bool ignoreUsingScope; readonly bool ignoreUsingScope;
readonly DecompilerSettings settings; readonly DecompilerSettings settings;
readonly TransformContext context;
CSharpResolver resolver; CSharpResolver resolver;
TypeSystemAstBuilder astBuilder; TypeSystemAstBuilder astBuilder;
@ -197,6 +204,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
public FullyQualifyAmbiguousTypeNamesVisitor(TransformContext context, UsingScope usingScope) public FullyQualifyAmbiguousTypeNamesVisitor(TransformContext context, UsingScope usingScope)
{ {
this.context = context;
this.ignoreUsingScope = !context.Settings.UsingDeclarations; this.ignoreUsingScope = !context.Settings.UsingDeclarations;
this.settings = context.Settings; this.settings = context.Settings;
this.resolver = new CSharpResolver(new CSharpTypeResolveContext(context.TypeSystem.MainModule)); this.resolver = new CSharpResolver(new CSharpTypeResolveContext(context.TypeSystem.MainModule));
@ -401,13 +409,31 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
} }
if (simpleType.Parent is Syntax.Attribute) if (simpleType.Parent is Syntax.Attribute)
{ {
simpleType.ReplaceWith(astBuilder.ConvertAttributeType(rr.Type)); ReplaceAndRecordStep("Qualify ambiguous attribute type", simpleType, astBuilder.ConvertAttributeType(rr.Type));
} }
else 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
base.VisitSyntaxTree(syntaxTree); base.VisitSyntaxTree(syntaxTree);
if (context.Settings.FileScopedNamespaces && singleNamespaceDeclaration != null) if (context.Settings.FileScopedNamespaces && singleNamespaceDeclaration != null)
{ {
context.Step("Use file-scoped namespace", singleNamespaceDeclaration);
singleNamespaceDeclaration.IsFileScoped = true; singleNamespaceDeclaration.IsFileScoped = true;
} }
} }
@ -106,7 +107,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{ {
if (statement is BlockStatement b && b.Statements.Count == 1 && IsAllowedAsEmbeddedStatement(b.Statements.First(), parent)) 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)) else if (!IsAllowedAsEmbeddedStatement(statement, parent))
{ {
@ -120,11 +124,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return parent is IfElseStatement && statement.Slot?.Kind == Slots.FalseStatement; return parent is IfElseStatement && statement.Slot?.Kind == Slots.FalseStatement;
} }
static void InsertBlock(Statement statement) void InsertBlock(Statement statement)
{ {
if (!(statement is BlockStatement)) if (!(statement is BlockStatement))
{ {
var b = new BlockStatement(); var b = new BlockStatement();
context.Step("Add block statement", statement);
statement.ReplaceWith(b); statement.ReplaceWith(b);
if (statement is EmptyStatement && !statement.HasChildren) if (statement is EmptyStatement && !statement.HasChildren)
{ {
@ -134,6 +139,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
{ {
b.Add(statement); b.Add(statement);
} }
context.EndStep(b);
} }
} }
@ -221,6 +227,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return; return;
if ((getter.Modifiers & ~movableModifiers) != 0) if ((getter.Modifiers & ~movableModifiers) != 0)
return; return;
context.Step("Use expression-bodied property", propertyDeclaration);
propertyDeclaration.Modifiers |= getter.Modifiers; propertyDeclaration.Modifiers |= getter.Modifiers;
propertyDeclaration.ExpressionBody = m.Get<Expression>("expression").Single().Detach(); propertyDeclaration.ExpressionBody = m.Get<Expression>("expression").Single().Detach();
propertyDeclaration.CopyAnnotationsFrom(getter); propertyDeclaration.CopyAnnotationsFrom(getter);
@ -236,6 +243,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
return; return;
if ((getter.Modifiers & ~movableModifiers) != 0) if ((getter.Modifiers & ~movableModifiers) != 0)
return; return;
context.Step("Use expression-bodied indexer", indexerDeclaration);
indexerDeclaration.Modifiers |= getter.Modifiers; indexerDeclaration.Modifiers |= getter.Modifiers;
indexerDeclaration.ExpressionBody = m.Get<Expression>("expression").Single().Detach(); indexerDeclaration.ExpressionBody = m.Get<Expression>("expression").Single().Detach();
indexerDeclaration.CopyAnnotationsFrom(getter); indexerDeclaration.CopyAnnotationsFrom(getter);

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

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

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

@ -63,9 +63,11 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (CanConvertToCompoundAssignment(assignment.Left) && assignment.Left.IsMatch(binary.Left) if (CanConvertToCompoundAssignment(assignment.Left) && assignment.Left.IsMatch(binary.Left)
&& binary.Right != null && IsImplicitlyConvertible(binary.Right, expectedType)) && binary.Right != null && IsImplicitlyConvertible(binary.Right, expectedType))
{ {
assignment.Operator = GetAssignmentOperatorForBinaryOperator(binary.Operator); var newOperator = GetAssignmentOperatorForBinaryOperator(binary.Operator);
if (assignment.Operator != AssignmentOperatorType.Assign) 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: // If we found a shorter operator, get rid of the BinaryOperatorExpression:
assignment.CopyAnnotationsFrom(binary); assignment.CopyAnnotationsFrom(binary);
assignment.Right = binary.Right; assignment.Right = binary.Right;
@ -88,7 +90,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
type = (assignment.Operator == AssignmentOperatorType.Add) ? UnaryOperatorType.PostIncrement : UnaryOperatorType.PostDecrement; type = (assignment.Operator == AssignmentOperatorType.Add) ? UnaryOperatorType.PostIncrement : UnaryOperatorType.PostDecrement;
else else
type = (assignment.Operator == AssignmentOperatorType.Add) ? UnaryOperatorType.Increment : UnaryOperatorType.Decrement; 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);
} }
} }
} }

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

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

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

@ -19,9 +19,11 @@
#nullable enable #nullable enable
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading; using System.Threading;
using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem;
namespace ICSharpCode.Decompiler.CSharp.Transforms namespace ICSharpCode.Decompiler.CSharp.Transforms
@ -36,6 +38,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
public readonly TypeSystemAstBuilder TypeSystemAstBuilder; public readonly TypeSystemAstBuilder TypeSystemAstBuilder;
public readonly DecompilerSettings Settings; public readonly DecompilerSettings Settings;
internal readonly DecompileRun DecompileRun; internal readonly DecompileRun DecompileRun;
public Stepper Stepper { get; set; }
readonly ITypeResolveContext decompilationContext; readonly ITypeResolveContext decompilationContext;
@ -67,6 +70,74 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
this.TypeSystemAstBuilder = typeSystemAstBuilder; this.TypeSystemAstBuilder = typeSystemAstBuilder;
this.CancellationToken = decompileRun.CancellationToken; this.CancellationToken = decompileRun.CancellationToken;
this.Settings = decompileRun.Settings; 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
var ci = new ConstructorInitializer { ConstructorInitializerType = type }; var ci = new ConstructorInitializer { ConstructorInitializerType = type };
context.Step("Move constructor call to initializer", stmt);
// Move arguments from invocation to initializer: // Move arguments from invocation to initializer:
invocation.GetChildren(Slots.Argument).MoveTo(ci.Arguments); invocation.GetChildren(Slots.Argument).MoveTo(ci.Arguments);
// Add the initializer: (unless it is the default 'base()') // Add the initializer: (unless it is the default 'base()')
@ -489,6 +490,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// Remove the statement // Remove the statement
stmt.Remove(); stmt.Remove();
context.EndStep(constructorDeclaration.Initializer);
return true; return true;
} }
@ -507,7 +509,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
// e.g. when a single static constructor is decompiled in isolation -- so the // e.g. when a single static constructor is decompiled in isolation -- so the
// assignment must remain in the constructor body. // assignment must remain in the constructor body.
if (kind is InitializerKind.Primary) if (kind is InitializerKind.Primary)
{
context.Step("Remove redundant primary constructor assignment", stmt);
stmt.Remove(); stmt.Remove();
}
continue; continue;
} }
@ -518,7 +523,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
v = fd.Variables.Single(); v = fd.Variables.Single();
if (v.Initializer is null) 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) else if (kind == InitializerKind.Static)
{ {
@ -548,7 +556,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
Debug.Assert(pd.IsAutomaticProperty); Debug.Assert(pd.IsAutomaticProperty);
if (pd.Initializer is null) 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 else
{ {
@ -560,7 +571,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
v = ev.Variables.Single(); v = ev.Variables.Single();
if (v.Initializer is null) 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 else
{ {
@ -594,6 +608,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (sequence.IsUnsafe && IntroduceUnsafeModifier.IsUnsafe(initializer)) if (sequence.IsUnsafe && IntroduceUnsafeModifier.IsUnsafe(initializer))
{ {
context.Step("Add unsafe modifier to initialized member", declaringSyntaxNode);
declaringSyntaxNode.Modifiers |= Modifiers.Unsafe; declaringSyntaxNode.Modifiers |= Modifiers.Unsafe;
} }
} }
@ -626,6 +641,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
var insertionPoint = (AstNode?)this.TypeDeclaration.TypeParameters.LastOrDefault() ?? this.TypeDeclaration.NameToken; var insertionPoint = (AstNode?)this.TypeDeclaration.TypeParameters.LastOrDefault() ?? this.TypeDeclaration.NameToken;
foreach (var param in PrimaryConstructorDecl.Parameters) foreach (var param in PrimaryConstructorDecl.Parameters)
{ {
context.Step("Move primary constructor parameter to type", param);
param.Remove(); param.Remove();
this.TypeDeclaration.InsertChildAfter(insertionPoint, param, Slots.Parameter); this.TypeDeclaration.InsertChildAfter(insertionPoint, param, Slots.Parameter);
insertionPoint = param; insertionPoint = param;
@ -680,6 +696,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (PrimaryConstructorDecl.HasModifier(Modifiers.Unsafe)) if (PrimaryConstructorDecl.HasModifier(Modifiers.Unsafe))
{ {
context.Step("Move unsafe modifier from primary constructor to type", this.TypeDeclaration);
this.TypeDeclaration.Modifiers |= Modifiers.Unsafe; this.TypeDeclaration.Modifiers |= Modifiers.Unsafe;
} }
@ -689,11 +706,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
var baseType = TypeDeclaration.BaseTypes.First(); var baseType = TypeDeclaration.BaseTypes.First();
var newBaseType = new InvocationAstType(); var newBaseType = new InvocationAstType();
context.Step("Move primary constructor initializer to base type", baseType);
baseType.ReplaceWith(newBaseType); baseType.ReplaceWith(newBaseType);
newBaseType.BaseType = baseType; newBaseType.BaseType = baseType;
initializer.Arguments.MoveTo(newBaseType.Arguments); initializer.Arguments.MoveTo(newBaseType.Arguments);
context.EndStep(newBaseType);
} }
context.Step("Remove primary constructor body", PrimaryConstructorDecl);
PrimaryConstructorDecl.Remove(); PrimaryConstructorDecl.Remove();
} }
@ -704,6 +724,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
if (IsBeforeFieldInit && StaticConstructorDecl.Body is { Statements.Count: 0 }) if (IsBeforeFieldInit && StaticConstructorDecl.Body is { Statements.Count: 0 })
{ {
context.Step("Remove empty static constructor", StaticConstructorDecl);
StaticConstructorDecl.Remove(); StaticConstructorDecl.Remove();
} }
} }
@ -733,9 +754,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
bool retainBecauseOfDocumentation = context.Settings.ShowXmlDocumentation bool retainBecauseOfDocumentation = context.Settings.ShowXmlDocumentation
&& context.DecompileRun.DocumentationProvider?.GetDocumentation(ctorMethod) != null; && context.DecompileRun.DocumentationProvider?.GetDocumentation(ctorMethod) != null;
if (!retainBecauseOfDocumentation) if (!retainBecauseOfDocumentation)
{
context.Step("Remove implicit constructor", ctor);
ctor.Remove(); ctor.Remove();
} }
} }
}
/// <summary> /// <summary>
/// Evaluates a call to the decimal-ctor. /// Evaluates a call to the decimal-ctor.

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

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

93
ILSpy.Tests/Views/DebugStepsTests.cs

@ -18,6 +18,7 @@
#if DEBUG #if DEBUG
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -27,10 +28,14 @@ using Avalonia.VisualTree;
using AwesomeAssertions; using AwesomeAssertions;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.ILSpy; using ICSharpCode.ILSpy;
using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Docking; using ICSharpCode.ILSpy.Docking;
using ICSharpCode.ILSpy.Languages; using ICSharpCode.ILSpy.Languages;
using ICSharpCode.ILSpy.TextView;
using ICSharpCode.ILSpy.TreeNodes; using ICSharpCode.ILSpy.TreeNodes;
using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpy.ViewModels;
using ICSharpCode.ILSpy.Views; using ICSharpCode.ILSpy.Views;
@ -119,6 +124,75 @@ public class DebugStepsTests
"after switching to ILAst and decompiling, the VM's Steps must list the stepper's recorded transforms"); "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] [AvaloniaTest]
public Task ILAst_And_TypedIL_Languages_Are_Registered_In_Debug_Builds() public Task ILAst_And_TypedIL_Languages_Are_Registered_In_Debug_Builds()
{ {
@ -133,6 +207,25 @@ public class DebugStepsTests
languageService.Languages.Should().Contain(l => l.Name == "Typed IL"); languageService.Languages.Should().Contain(l => l.Name == "Typed IL");
return Task.CompletedTask; 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] [AvaloniaTest]
public Task Pane_Reports_Not_Available_For_Languages_Without_Debug_Steps() public Task Pane_Reports_Not_Available_For_Languages_Without_Debug_Steps()
{ {

7
ILSpy/DecompilationOptions.cs

@ -60,6 +60,13 @@ namespace ICSharpCode.ILSpy
/// </summary> /// </summary>
public int StepLimit { get; set; } = int.MaxValue; 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> /// <summary>
/// When true, transforms emit verbose debug information about their behaviour. Only /// When true, transforms emit verbose debug information about their behaviour. Only
/// meaningful in combination with <see cref="StepLimit"/> — the Debug Steps pane sets /// 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
//this.externAliasKeywordColor = ...; //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) public override void WriteKeyword(string keyword)
{ {
HighlightingColor? color = null; HighlightingColor? color = null;
@ -496,6 +502,7 @@ namespace ICSharpCode.ILSpy.Languages
public override void StartNode(AstNode node) public override void StartNode(AstNode node)
{ {
nodeTrackingOutput?.MarkNodeStart(node);
nodeStack.Push(node); nodeStack.Push(node);
base.StartNode(node); base.StartNode(node);
} }
@ -503,6 +510,7 @@ namespace ICSharpCode.ILSpy.Languages
public override void EndNode(AstNode node) public override void EndNode(AstNode node)
{ {
base.EndNode(node); base.EndNode(node);
nodeTrackingOutput?.MarkNodeEnd(node);
nodeStack.Pop(); nodeStack.Pop();
} }
@ -511,6 +519,7 @@ namespace ICSharpCode.ILSpy.Languages
int currentColorBegin = -1; int currentColorBegin = -1;
readonly ILocatable? locatable; readonly ILocatable? locatable;
readonly ISmartTextOutput? textOutput; readonly ISmartTextOutput? textOutput;
readonly AvaloniaEditTextOutput? nodeTrackingOutput;
// Wraps a base WriteX call so its output lands inside a highlighting span for the given colour // 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. // (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;
namespace ICSharpCode.ILSpy.Languages namespace ICSharpCode.ILSpy.Languages
{ {
/// <summary> /// <summary>
/// Debug Steps support for the C# language: coarse, one step per AST transform, shown in the /// Debug Steps support for the C# language, shown in the Debug Steps pane like the ILAst
/// Debug Steps pane like the ILAst language already does for IL transforms. The step list is /// language already does for IL transforms. A full decompile records AST transform groups with
/// static -- the C# AST pipeline (<see cref="CSharpDecompiler.GetAstTransforms"/>) is the same /// individual mutation steps inside; a selected step's index is replayed by re-decompiling with <see
/// for every member -- so a selected step's index maps straight onto <see /// cref="DecompilationOptions.StepLimit"/>.
/// cref="DecompilationOptions.StepLimit"/>, which CreateDecompiler turns into the number of AST
/// transforms to keep before re-rendering.
/// </summary> /// </summary>
partial class CSharpLanguage : IDebugStepProvider partial class CSharpLanguage : IDebugStepProvider
{ {
Stepper? stepper; Stepper stepper = new Stepper();
public Stepper Stepper => stepper ??= BuildStepper(); public Stepper Stepper => stepper;
public event EventHandler? StepperUpdated; public event EventHandler? StepperUpdated;
@ -51,18 +49,7 @@ namespace ICSharpCode.ILSpy.Languages
// tree for C#, unlike ILAst's writing-options checkboxes. // tree for C#, unlike ILAst's writing-options checkboxes.
public object? StepOptions => null; public object? StepOptions => null;
// One node per AST transform, in pipeline order. Stepper.Step assigns BeginStep=i / partial void OnCSharpDecompiled(CSharpDecompiler decompiler, ITextOutput output, DecompilationOptions options)
// 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)
{ {
// The button always shows so the pane is one click away; mirrors the ILAst language. // 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 // DockWorkspace is resolved lazily (an ImportingConstructor import would form a MEF
@ -74,7 +61,7 @@ namespace ICSharpCode.ILSpy.Languages
// pane itself) must leave the tree and the user's selection intact. // pane itself) must leave the tree and the user's selection intact.
if (options.StepLimit == int.MaxValue) if (options.StepLimit == int.MaxValue)
{ {
_ = Stepper; stepper = decompiler.Stepper;
StepperUpdated?.Invoke(this, EventArgs.Empty); StepperUpdated?.Invoke(this, EventArgs.Empty);
} }
} }

89
ILSpy/Languages/CSharpLanguage.cs

@ -33,6 +33,7 @@ using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.CSharp.Transforms;
using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.IL.Transforms;
using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.Output; using ICSharpCode.Decompiler.Output;
using ICSharpCode.Decompiler.Solution; using ICSharpCode.Decompiler.Solution;
@ -313,11 +314,8 @@ namespace ICSharpCode.ILSpy.Languages
CancellationToken = options.CancellationToken, CancellationToken = options.CancellationToken,
DebugInfoProvider = module.GetDebugInfoOrNull(), DebugInfoProvider = module.GetDebugInfoOrNull(),
}; };
// The Debug Steps pane stops the AST pipeline at a chosen step by re-decompiling with decompiler.Stepper.StepLimit = options.StepLimit;
// options.StepLimit = number of AST transforms to keep; pop the rest from the end. decompiler.Stepper.IsDebug = options.IsDebug;
// 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);
if (options.EscapeInvalidIdentifiers) if (options.EscapeInvalidIdentifiers)
decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers()); decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers());
return decompiler; return decompiler;
@ -345,25 +343,25 @@ namespace ICSharpCode.ILSpy.Languages
{ {
var members = CollectFieldsAndCtors(methodDefinition.DeclaringTypeDefinition!, methodDefinition.IsStatic); var members = CollectFieldsAndCtors(methodDefinition.DeclaringTypeDefinition!, methodDefinition.IsStatic);
decompiler.AstTransforms.Add(new SelectCtorTransform(methodDefinition)); decompiler.AstTransforms.Add(new SelectCtorTransform(methodDefinition));
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(members)); WriteCode(output, options, decompiler.Decompile(members), decompiler);
} }
else 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; // Implemented only under DEBUG (CSharpLanguage.DebugSteps.cs) to feed the Debug Steps pane;
// a no-op partial in Release. // 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) public override void DecompileProperty(IProperty property, ITextOutput output, DecompilationOptions options)
{ {
CSharpDecompiler decompiler = BeginDecompile(property, output, options); CSharpDecompiler decompiler = BeginDecompile(property, output, options);
WriteCommentLine(output, TypeToString(property.DeclaringType)); WriteCommentLine(output, TypeToString(property.DeclaringType));
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(property.MetadataToken)); WriteCode(output, options, decompiler.Decompile(property.MetadataToken), decompiler);
OnCSharpDecompiled(output, options); OnCSharpDecompiled(decompiler, output, options);
} }
public override void DecompileField(IField field, ITextOutput output, DecompilationOptions options) public override void DecompileField(IField field, ITextOutput output, DecompilationOptions options)
@ -372,16 +370,16 @@ namespace ICSharpCode.ILSpy.Languages
WriteCommentLine(output, TypeToString(field.DeclaringType)); WriteCommentLine(output, TypeToString(field.DeclaringType));
if (field.IsConst) if (field.IsConst)
{ {
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(field.MetadataToken)); WriteCode(output, options, decompiler.Decompile(field.MetadataToken), decompiler);
} }
else else
{ {
var members = CollectFieldsAndCtors(field.DeclaringTypeDefinition!, field.IsStatic); var members = CollectFieldsAndCtors(field.DeclaringTypeDefinition!, field.IsStatic);
var resolvedField = decompiler.TypeSystem.MainModule.GetDefinition((FieldDefinitionHandle)field.MetadataToken); var resolvedField = decompiler.TypeSystem.MainModule.GetDefinition((FieldDefinitionHandle)field.MetadataToken);
decompiler.AstTransforms.Add(new SelectFieldTransform(resolvedField)); 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> /// <summary>
@ -408,24 +406,24 @@ namespace ICSharpCode.ILSpy.Languages
CSharpDecompiler decompiler = BeginDecompile(extension, output, options); CSharpDecompiler decompiler = BeginDecompile(extension, output, options);
WriteCommentLine(output, TypeToString(commentType, WriteCommentLine(output, TypeToString(commentType,
ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.SupportExtensionDeclarations)); ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.SupportExtensionDeclarations));
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.DecompileExtension(extension.MetadataToken)); WriteCode(output, options, decompiler.DecompileExtension(extension.MetadataToken), decompiler);
OnCSharpDecompiled(output, options); OnCSharpDecompiled(decompiler, output, options);
} }
public override void DecompileEvent(IEvent ev, ITextOutput output, DecompilationOptions options) public override void DecompileEvent(IEvent ev, ITextOutput output, DecompilationOptions options)
{ {
CSharpDecompiler decompiler = BeginDecompile(ev, output, options); CSharpDecompiler decompiler = BeginDecompile(ev, output, options);
WriteCommentLine(output, TypeToString(ev.DeclaringType)); WriteCommentLine(output, TypeToString(ev.DeclaringType));
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(ev.MetadataToken)); WriteCode(output, options, decompiler.Decompile(ev.MetadataToken), decompiler);
OnCSharpDecompiled(output, options); OnCSharpDecompiled(decompiler, output, options);
} }
public override void DecompileType(ITypeDefinition type, ITextOutput output, DecompilationOptions options) public override void DecompileType(ITypeDefinition type, ITextOutput output, DecompilationOptions options)
{ {
CSharpDecompiler decompiler = BeginDecompile(type, output, options); CSharpDecompiler decompiler = BeginDecompile(type, output, options);
WriteCommentLine(output, TypeToString(type, ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames)); WriteCommentLine(output, TypeToString(type, ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames));
WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(type.MetadataToken)); WriteCode(output, options, decompiler.Decompile(type.MetadataToken), decompiler);
OnCSharpDecompiled(output, options); OnCSharpDecompiled(decompiler, output, options);
} }
public override ProjectId? DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) public override ProjectId? DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options)
@ -511,7 +509,7 @@ namespace ICSharpCode.ILSpy.Languages
SyntaxTree st = options.FullDecompilation SyntaxTree st = options.FullDecompilation
? decompiler.DecompileWholeModuleAsSingleFile() ? decompiler.DecompileWholeModuleAsSingleFile()
: decompiler.DecompileModuleAndAssemblyAttributes(); : decompiler.DecompileModuleAndAssemblyAttributes();
WriteCode(output, options.DecompilerSettings, decompiler, st); WriteCode(output, options, st, decompiler);
return null; return null;
} }
@ -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 }); syntaxTree.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true });
output.IndentationString = settings.CSharpFormattingOptions.IndentationString; output.IndentationString = settings.CSharpFormattingOptions.IndentationString;
TokenWriter tokenWriter = new TextTokenWriter(output, settings, decompiler.TypeSystem); 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); tokenWriter = new CSharpHighlightingTokenWriter(tokenWriter, smartOutput);
// For the on-screen C# view, harvest the IL-offset/line map for body bookmarks during this // 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
syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions)); syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions));
bookmarkCollector?.Publish(); 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, void AddWarningMessage(MetadataFile module, ITextOutput output, string line1, string? line2 = null,

20
ILSpy/TextView/AvaloniaEditTextOutput.cs

@ -54,6 +54,7 @@ namespace ICSharpCode.ILSpy.TextView
public int LengthLimit { get; set; } = int.MaxValue; public int LengthLimit { get; set; } = int.MaxValue;
readonly Stack<(int Offset, HighlightingColor Color)> openSpans = new(); 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 Stack<(NewFolding Folding, int StartLine)> openFoldings = new();
readonly List<NewFolding> foldings = new(); readonly List<NewFolding> foldings = new();
int indent; int indent;
@ -87,6 +88,10 @@ namespace ICSharpCode.ILSpy.TextView
/// <summary>Maps reference targets to their definition offsets in the rendered text.</summary> /// <summary>Maps reference targets to their definition offsets in the rendered text.</summary>
public DefinitionLookup DefinitionLookup { get; } = new(); public DefinitionLookup DefinitionLookup { get; } = new();
internal NodeLookup NodeLookup { get; } = new();
internal TextRange? DebugStepHighlight { get; set; }
readonly List<Bookmarks.MethodDebugInfo> methodDebugInfos = new(); readonly List<Bookmarks.MethodDebugInfo> methodDebugInfos = new();
/// <summary> /// <summary>
@ -300,5 +305,20 @@ namespace ICSharpCode.ILSpy.TextView
highlightingSpans.Add((start, length, color)); 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
[ObservableProperty] [ObservableProperty]
private DefinitionLookup? definitionLookup; private DefinitionLookup? definitionLookup;
[ObservableProperty]
private TextRange? debugStepHighlight;
/// <summary> /// <summary>
/// IL-offset &lt;-&gt; line maps for the methods in this document (C# only). Lets bookmarks /// 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# /// 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
// every run; set non-default by RestartDecompileWithStepLimit before kicking off a // every run; set non-default by RestartDecompileWithStepLimit before kicking off a
// debug-stepper decompile. Set on the UI thread only — no inter-thread access. // debug-stepper decompile. Set on the UI thread only — no inter-thread access.
int pendingStepLimit = int.MaxValue; int pendingStepLimit = int.MaxValue;
int? pendingHighlightStep;
bool pendingIsDebug; bool pendingIsDebug;
/// <summary> /// <summary>
@ -397,9 +401,10 @@ namespace ICSharpCode.ILSpy.TextView
/// is <see cref="int.MaxValue"/>). <paramref name="isDebug"/> toggles the transforms' /// is <see cref="int.MaxValue"/>). <paramref name="isDebug"/> toggles the transforms'
/// verbose-debug emission. No-op when there's nothing currently being decompiled. /// verbose-debug emission. No-op when there's nothing currently being decompiled.
/// </summary> /// </summary>
public void RestartDecompileWithStepLimit(int stepLimit, bool isDebug) public void RestartDecompileWithStepLimit(int stepLimit, bool isDebug, int? highlightStep = null)
{ {
pendingStepLimit = stepLimit; pendingStepLimit = stepLimit;
pendingHighlightStep = highlightStep;
pendingIsDebug = isDebug; pendingIsDebug = isDebug;
StartDecompile(); StartDecompile();
} }
@ -511,6 +516,7 @@ namespace ICSharpCode.ILSpy.TextView
References = null; References = null;
DefinitionLookup = null; DefinitionLookup = null;
DebugInfo = null; DebugInfo = null;
DebugStepHighlight = null;
UIElements = null; UIElements = null;
Text = string.Empty; Text = string.Empty;
IsDecompiling = false; IsDecompiling = false;
@ -537,8 +543,10 @@ namespace ICSharpCode.ILSpy.TextView
// stable value, and reset the fields to defaults so the NEXT decompile runs // stable value, and reset the fields to defaults so the NEXT decompile runs
// at full fidelity unless RestartDecompileWithStepLimit sets them again. // at full fidelity unless RestartDecompileWithStepLimit sets them again.
var stepLimit = pendingStepLimit; var stepLimit = pendingStepLimit;
var highlightStep = pendingHighlightStep;
var isDebug = pendingIsDebug; var isDebug = pendingIsDebug;
pendingStepLimit = int.MaxValue; pendingStepLimit = int.MaxValue;
pendingHighlightStep = null;
pendingIsDebug = false; pendingIsDebug = false;
var outputLengthLimit = pendingOutputLengthLimit; var outputLengthLimit = pendingOutputLengthLimit;
pendingOutputLengthLimit = DefaultOutputLengthLimit; pendingOutputLengthLimit = DefaultOutputLengthLimit;
@ -553,6 +561,7 @@ namespace ICSharpCode.ILSpy.TextView
decompilerSettings ?? new ICSharpCode.Decompiler.DecompilerSettings()) { decompilerSettings ?? new ICSharpCode.Decompiler.DecompilerSettings()) {
CancellationToken = cts.Token, CancellationToken = cts.Token,
StepLimit = stepLimit, StepLimit = stepLimit,
HighlightStep = highlightStep,
IsDebug = isDebug, IsDebug = isDebug,
}; };
try try
@ -745,6 +754,7 @@ namespace ICSharpCode.ILSpy.TextView
: output.MethodDebugInfos.Count > 0 : output.MethodDebugInfos.Count > 0
? new Bookmarks.DecompiledDebugInfo(output.MethodDebugInfos) ? new Bookmarks.DecompiledDebugInfo(output.MethodDebugInfos)
: Bookmarks.DecompiledDebugInfo.Empty; : Bookmarks.DecompiledDebugInfo.Empty;
DebugStepHighlight = output.DebugStepHighlight;
UIElements = output.UIElements; UIElements = output.UIElements;
Text = text; Text = text;
} }

30
ILSpy/TextView/DecompilerTextView.axaml.cs

@ -64,6 +64,7 @@ namespace ICSharpCode.ILSpy.TextView
// softer green for the actual definition. // softer green for the actual definition.
static readonly Color LocalMatchBackground = Colors.GreenYellow; static readonly Color LocalMatchBackground = Colors.GreenYellow;
static readonly Color LocalDefinitionBackground = Color.FromArgb(0x80, 0xA0, 0xFF, 0xA0); 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 // 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 // `distanceToPopupLimit` to the popup edges, the popup stays. The limit shrinks toward
@ -78,6 +79,7 @@ namespace ICSharpCode.ILSpy.TextView
TextMarkerService textMarkerService = null!; TextMarkerService textMarkerService = null!;
BracketHighlightRenderer bracketHighlightRenderer = null!; BracketHighlightRenderer bracketHighlightRenderer = null!;
readonly List<TextMarker> localReferenceMarks = new(); readonly List<TextMarker> localReferenceMarks = new();
readonly List<TextMarker> debugStepMarks = new();
readonly List<AvaloniaEdit.Rendering.VisualLineElementGenerator> activeCustomGenerators = new(); readonly List<AvaloniaEdit.Rendering.VisualLineElementGenerator> activeCustomGenerators = new();
RichTextColorizer? activeColorizer; RichTextColorizer? activeColorizer;
FoldingManager? activeFoldingManager; FoldingManager? activeFoldingManager;
@ -1043,6 +1045,7 @@ namespace ICSharpCode.ILSpy.TextView
// force-close even if the popup currently wants to stay (mouseClick: true). // force-close even if the popup currently wants to stay (mouseClick: true).
TryCloseExistingPopup(mouseClick: true); TryCloseExistingPopup(mouseClick: true);
ClearLocalReferenceMarks(); ClearLocalReferenceMarks();
ClearDebugStepMarks();
Editor.SyntaxHighlighting = HighlightingService.GetByExtension(model.SyntaxExtension); Editor.SyntaxHighlighting = HighlightingService.GetByExtension(model.SyntaxExtension);
Editor.Document.Text = model.Text; Editor.Document.Text = model.Text;
@ -1072,6 +1075,8 @@ namespace ICSharpCode.ILSpy.TextView
if (restoreViewState) if (restoreViewState)
RestoreOrResetViewState(pendingState); RestoreOrResetViewState(pendingState);
ApplyDebugStepHighlight(model.DebugStepHighlight);
// Position at a navigated-to bookmark once its document (and debug map) has landed. Only on // 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 // 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 // reset above, so the bookmark scroll is the last word on position and the highlight plays
@ -1246,6 +1251,31 @@ namespace ICSharpCode.ILSpy.TextView
localReferenceMarks.Clear(); 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 // 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 // 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. // column when the user is hovering empty trailing space.

50
ILSpy/TextView/NodeLookup.cs

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

Loading…
Cancel
Save