postsharp / Metalama

Metalama is a Roslyn-based meta-programming framework. Use this repo to report bugs or ask questions.
176 stars 5 forks source link

Feature request: ability to add breakpoints in run-time code generated by aspects #133

Closed WhitWaldo closed 1 year ago

WhitWaldo commented 1 year ago

I have been working on adding unit tests to validate that the aspects applied to types and methods are working as expected by mocking injected dependencies and validating that they're capturing what they should.

However, as I add new functionality, I frequently run into build errors that are reported by the error bar in Visual Studio. This is fine - I can usually figure out what's going wrong, but it gets more cumbersome when the aspect builds correctly but the output in the preview doesn't match what I'd like it to.

I'd like to ask for some sort of debugging step-through experience in which I can step through the code as it's applied at compile time so I can more richly understand how Metalama is doing what it's doing, identify richer opportunities for unit testing and more quickly troubleshoot and resolve bugs myself without resorting to a support ticket.

Ideally, this would be little different than setting a breakpoint in a normal .NET application - I'd set a breakpoint in the aspect and when the project next rebuilt behind the scenes, the breakpoint would be hit for one of the applications of the aspect and I'd have the opportunity to step through and investigate the value of each compile-time variable.

Thank you for the consideration!

PostSharpBot commented 1 year ago

Hello @WhitWaldo, thank you for submitting this issue. We will try to get back to you as soon as possible. Note to the PostSharp team, this ticket is being tracked in our dashboard under ID TP-33185.

gfraiteur commented 1 year ago

Whit,

It seems that you feature request is partly covered by the LamaDebug configuration as explained in https://doc.metalama.net/conceptual/using/debugging-aspect-oriented-code. The main gap I see is that breakpoints must be set in transformed code and not in the aspect code.

It would be very complex and perhaps impossible to allow you to set run-time breakpoints in aspect code using the UI because one line of aspect code maps to multiple lines of code in transformed code.

Could you please elaborate on what your request differs from the LamaDebug feature?

WhitWaldo commented 1 year ago

I'll dig into the documentation you linked and see if it's sufficient for what I had in mind and get back to you.

WhitWaldo commented 1 year ago

So, here's the problem I'm currently looking at. I'm looking at building out the behavior of a Blazor component based on the attributes applied to the type:

public abstract record EditStartAction();

public sealed class CanEdit<TEditAction> : Attribute
  where TEditAction : EditStartAction, new()
{
}

public record MyCreateStartAction : CreateStartAction
{
}

[GenerateBehavior]
[CanEdit<MyCreateStartAction>]
public partial class TestTable : FluxorComponent
{
}

public sealed class GenerateBehavior : TypeAspect
{
  public override void BuildAspect(IAspectBuilder<INamedType> builder)
  {
    var supportsEditing = builder.Target.Attributes.OfAttributeType(typeof(CanEditAttribute<>)).Any();

    if (supportsEditing) 
    {
      //Introduce a method
    }
  }
}

When the aspect builds, I don't see the method show up in the preview even though the attribute is applied. I'd really like to put a breakpoint on the line on which I'm assigning the value to supportsEditing so I can work to understand why it's not spotting that attribute on the type (and if it's picking up anything at all. Ideally, Metalama would either indicate that nothing implements the aspect or it would pick precisely one of the targets (ideally the same one each time in order) and allow me to step through the aspect as it applied to the compile process so I might debug it like any other program.

gfraiteur commented 1 year ago

Ok, I have renamed the ticket to what I think better describes what you wish.

This is very complex and I'm not even sure there is a good solution for this.

What you may do is to add a call to Debugger.Break in your aspect call. This would actually define an unconditional breakpoint and you would have to recompile to remove it.

Another approach may be to have a debugging helper class like this:

static class DebuggingHelper
{
   public static void ConditionalBreak( string aspectName, string targetTypeName, string targetMemberName )
  {
    // Put a conditional breakpoint with your UI here as needed.
   }
}

And you would inject a call to this helper using the aspect:

DebuggingHelper.ConditionalBreak( "TheAspect", meta.Target.Type.Name, meta.Target.Member.Name );

Then, using the UI of your IDE, you could set/unset breakpoints as needed and set a condition on the different parameters of the aspect.

Would that cover your needs?

WhitWaldo commented 1 year ago

While I might need to debug the run-time code, I can more easily do that by simply writing a unit test that invokes it, setting a breakpoint on the unit test entry and then step through it and that worked pretty well for the unit tests I've written thus far.

No, here, I'm looking more for something that would let me step through the generation of the run-time code when the aspect is doing its compilation step.

Let me share an example and what I've been resorting to to try to address this.

Say I've got an entity:

public interface IIdentifiedObject 
{
  public Guid Id { get; init; }
}

public sealed record Vehicle(Guid Id, string Name, int DoorCount) : IIdentifiedObject;

public abstract class MyBaseClass<TEntity, TId>
  where TEntity : class, IIdentifiedObject
  where TId : IEquatable<TId>, IComparable, IComparable<TId>
{
  protected MyBaseClass(Uri identifier)
  {
    //...
  }
}

public sealed class MyType : MyBaseClass<Vehicle, Guid>
{
  public MyType() : base(new Uri("data://entities/vehicle") {}
}

Then we've got my aspect I'm working on:

[AttributeUsage(AttributeTargets.Class)]
public sealed class IndexAttribute : TypeAspect
{
  public override void BuildAspect(IAspectBuilder<INamedType> builder)
  {
    var entityType = builder.Target.BaseType.TypeParameters[0];
    var idType = builder.Target.BaseType.TypeParameters[1];
  }
}

Now, initially I neglected to include the BaseType bit so it wasn't actually getting the type parameters out of it. Understandable. But the error simply indicated an index out of range error and not that it was an issue with line X from my aspect.

It would have saved me a lot of time if I could simply put a breakpoint on the line starting with var entityType and step through the compilation so I could see that the builder.Target.TypeParameters collection was actually empty and that's the heart of this inquiry. Not to add breakpoints in the run-time code but to add breakpoints to the compile-time code so I can more readily understand when they break, why it is that they break.

Rather, I've started advising new methods into the type that reflect whatever piece it is I'm working on. For example, continuing from above:

builder.Advice.IntroduceMethod(builder.Target, nameof(DebugMethod), IntroductionScope.Instance, OverrideStrategy.Ignore, b => { b.Accessibility = Accessibility.Private }, args : new {
  TEntityType = entityType,
  TIdType = idType
});

//...and later on in the class:

[Template]
private void DebugMethod<[CompileTime]TEntityType, [CompileTime] TIdType>()
{
  var entityTypeName = typeof(TEntityType).Name;
  var entityId = typeof(TIdType).Name;
  Console.WriteLine($"{entityTypeName} - {entityId}");
}

For lack of a breakpoint then and all subsequent code commented out, this gives me a way to validate that what I think is being manipulated on in the aspect is indeed what I think it is.. by looking at the compiled output in the method. This is workable, but far from ideal as these aspects get increasingly complicated.

Again, I fully acknowledge that this is likely a really heavy ask, but it would vastly improve the experience of writing more elaborate aspects that are breaking without relying on filing tickets for every little issue I run into along the way.

Thank you!

WhitWaldo commented 1 year ago

Take another one I just did - checking to see what the names are of the fields or properties on the type:

//Aspect
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
  builder.Advice.IntroduceMethod(builder.Target, nameof(DebugMethod), IntroductionScope.Instance, OverrideStrategy.Ignore, 
    b => {b.Accessibility = Accessibility.Private; }, args: new {
        collection = builder.Target.AllFieldsAndProperties
    });
}

//...

    [Template]
    private void DebugMethod(IFieldOrPropertyCollection collection)
    {
        foreach (var item in collection)
        {
            Console.WriteLine(item.Name);
        }
    }

Produces this - very helpful for my purposes to know that I'm on the right track: image

gfraiteur commented 1 year ago

To add a breakpoint to a compile-time method, add Debugger.Break() to this method. In a template method, use meta.DebugBreak() Instead of using Console.WriteLine you can do meta.InsertComment.