AArnott / CodeGeneration.Roslyn

Assists in performing Roslyn-based code generation during a build.
Microsoft Public License
408 stars 59 forks source link

How to debug and troubleshoot my code generator #76

Closed mwpowellhtx closed 5 years ago

mwpowellhtx commented 6 years ago

Basically that. Is there some sort of "print line", console output, etc, I could wire into?

I see evidence that the generator is at least trying, literally the .generated.cs file is landing in my obj/$(Configuration) folder. However, what I expect to be generated is not being generated, but I'd at least like to get some feedback as to what is happening.

Do you have a Gitter I could post to?

mwpowellhtx commented 6 years ago

Notes for above; the generated file is there, the comments are there, but there is no output. But I assure you there is a tree I am building which should be there.

Ideally, I should be able to examine the output of the test assembly for the Type of interest, the expected elements, methods, constructors, etc, whether public, private, static, or what have you. But if the generator is not being invoked, how do I detect that? Never mind what the solution is.

Consequently, I am also chewing on a Generators parallel to the Analyzers Diagnostic Verifiers, but I wonder if that is even possible, considering whether it is possible to do a couple of things:

Assuming presence of the Microsoft.CodeAnalysis.Project instance. It is possible, for instance, to reference a concrete Assembly MetadataReference, but I do not think there is one for BuildTime?

But basically, somewhere between the given source code, and the compilation result, i.e.

Project project;
// Ostensibly, my Generator is invoked during this call:
var diagnostics = project.GetCompilationAsync().Result.GetDiagnostics();
// Do something with the Diagnostics...

And/or, ostensibly, additional analytics on the built source tree. Which, I assume would invoke the generator.

Short of this approach, would a "simple" Console.WriteLine be sufficient? Something which I could at least scan the build output to determine whether I am even hitting the generator in the first place? Or other CodeGeneration.Roslyn logs which would yield as much?

amis92 commented 6 years ago

I'm no expert, but I've had my share of experience with this codebase, allow me to iron out the way this framework/tool works.

First, let's go through all of the artifacts this project (CodeGeneration.Roslyn) produces:

Now, armed with understanding of the CodeGeneration framework structure, we can analyze a typical code generation-involving build process.

  1. MSBuild begins a build.
  2. After MSBuild discovers all source files, resolves assembly references and does all other necessary steps, the MSBuild target defined in .targets file from CodeGeneration.Roslyn.BuildTime package is resolved.
    1. This target calls GenerateCodeFromAttributes MSBuild Task, which invokes dotnet-codegen executable with appropriately-prepared argument list.
    2. dotnet-codegen executalbe parses arguments and after assigning appropriate values to CompilationGenerator's properties, invokes it's method.
    3. CompilationGenerator does it's job, and assigns results to its output properties.
    4. dotnet-codegen writes the generated-file-list to file.
    5. MSBuild task outputs the list of generated files read from the file.
  3. MSBuild target adds generated files to the Compile Item and continues on to the next build steps.

To clear up:

I hope this helps somewhat.

PS @AArnott having put a bit of effort into writing this down, would you consider using this comment somewhere in wiki or in README, pending additions and modifications you deem necessary? I'm inclined to believe it could help some more people get a clearer picture of how this framework actually works. :)

mwpowellhtx commented 6 years ago

In general, I've got an idea how it works, but I wonder if the DotNetCli references could at all be migrated to NuGet style package references? In other words, make the whole code-gen process more seamless. That would certainly help with the dependencies, keeping the whole pipeline flowing. That, or there being first class support for propagating DotNetCli references via NuGet, somehow...

amis92 commented 6 years ago

@mwpowellhtx No, DotNetCliTools are by design non-propagating. They are project-level, development-time only tools, and are useful only during the build of the project in which they are referenced. Other similar tools are EntityFrameworkCore tool, XmlSerializer tool etc.

That said, if you're referencing the issue of including the reference to the tool package, then that's a whole other story - for example it could be "done" (more like "hacked" but it'd work) by preparing an MSBuild .props file that'd add a reference to it. But that works only for direct references (including packages props and targets into build) - for indirectly referenced packages it doesn't.

So if you're planning to distribute your custom generator package, currently it's rather hard. You could hack up a props/targets and include that in your package, but it'll likely cause conflicts if the consumer references more than one code-generating package.

That said, this project is not yet quite stable regarding the API and generators based on different dotnet-codegen tool will likely crash anyway.

mwpowellhtx commented 6 years ago

@amis92 Yes, I understand that. But what I'm suggesting, what I wonder about, is whether the NuGet package reference PrivateAssets="All", or development only, propagation couldn't be leveraged here?

It would certainly make unit testing a heck of a lot easier, I think. That said concerning Code Gen, per se...

Concerning Verifiers efforts, yes, I am breaking up the dependencies a bit to better support at least a compilation-oriented treatment of my generator callbacks. I'd followed a similar approach as you did concerning calling helper Generate methods and so forth. That should make at least those somewhat testable, just short of integration bits.

For this purpose, I wouldn't need Code Fixes, per se, so to allow some flexibility whether analyzers are required, and so on. In this instance, all I need is the compilation result, diagnostic, etc. Anyway, that's a bit tangential to this, apart from ensuring my integration scaffold is in order.

mwpowellhtx commented 6 years ago

Following up here, I am at a point, I have the Microsoft.CodeAnalysis.AttributeData in hand after successful compilation. If I create a new MyGenerator(myAttrData), and subsequently invoke the GenerateAsync(...) method, this assumes that I land with appropriate TransformationContext and potentially also IProgress<Diagnostic>. However, where do I get those from?

Short of that, sort of integration tests, I don't know if it is appropriate from a unit test perspective to simply test whatever underlying bits are contributing to the generation of the SyntaxList<MemberDeclarationSyntax>. At least not without incurring the Code Generation overhead in a unit test context, which is kind of why I'm here in this issue in the first place; cutting out that glue as much as possible in order to verify whether code generation is happening at all.

Assuming that it is, why doesn't it land in the expected generated code documents?

mwpowellhtx commented 6 years ago

It seems I can do something like this:

var context = new CodeGeneration.Roslyn.TransformationContext(classDecl
    , compilation.GetSemanticModel(compilation.SyntaxTrees.First()), compilation, ProjectDirectory);

var members = g.GenerateAsync(context, new DiagnosticProgress(), CancellationToken.None).Result;

But this is generating something that clearly is not correct. Does this tell me I must even provide white space elements? The really important bits are there, to be sure; but the white space elements, new lines, etc, are not. Really?

  | Name | Value | Type
-- | -- | -- | --
  | $"{members}" | "partialclassCardinalDirection{privateCardinalDirection(byte[]bytes):base(bytes){}publicstaticCardinalDirectionoperator~(CardinalDirectionother)=>other?.BitwiseNot();publicstaticCardinalDirectionoperator&(CardinalDirectiona,CardinalDirectionb)=>a?.BitwiseAnd(b);publicstaticCardinalDirectionoperator\|(CardinalDirectiona,CardinalDirectionb)=>a?.BitwiseOr(b);publicstaticCardinalDirectionoperator^(CardinalDirectiona,CardinalDirectionb)=>a?.BitwiseXor(b);}" | string
mwpowellhtx commented 6 years ago

Okay, with .NormalizeWhitespace, thank goodness! :+1:

mwpowellhtx commented 6 years ago

Alright, so at this point, I am fairly confident that everything from the CSharpCompilation is good, and everything in the ICodeGenerator itself is good, if acceptable. I have not tested deep in the generated code yet, but I can tell from visual inspection that it is good, as expected.

Literally, what is generated is this:

  | Name | Value | Type
-- | -- | -- | --
  | $"{members}" | "partial class CardinalDirection\r\n{\r\n    private CardinalDirection(byte[] bytes): base(bytes)\r\n    {\r\n    }\r\n\r\n    public static CardinalDirection operator ~(CardinalDirection other) => other?.BitwiseNot();\r\n    public static CardinalDirection operator &(CardinalDirection a, CardinalDirection b) => a?.BitwiseAnd(b);\r\n    public static CardinalDirection operator \|(CardinalDirection a, CardinalDirection b) => a?.BitwiseOr(b);\r\n    public static CardinalDirection operator ^(CardinalDirection a, CardinalDirection b) => a?.BitwiseXor(b);\r\n}" | string

So... Something glue-wise, in between, is not working.

mwpowellhtx commented 6 years ago

Ugh, there's also a whole slew of possible combinations for parent syntax declarations that could happen. Such as,

namespace MyClasses { }
namespace My.Classes { }
namespace My { namespace Classes { } }

To name a few. Which all has an impact on the syntax that must be digested. I'll have to study that in more depth I think for the actual semantic model, how it breaks down.

Resulting syntax elements are something like, Json-ish in pseudo code:

{ "MyClasses" : NamespaceDeclarationSyntax }
{ "My.Classes" : NamespaceDeclarationSyntax }

And,

{ "My" : NamespaceDeclarationSyntax { "Classes" : NamespaceDeclarationSyntax } }

In the last couple of use cases, I might expect "My.Classes" to fall out in the semantic model, however.

amis92 commented 6 years ago

I don't know what's not working for you, but I can assure you that Roslyn.CodeGeneration takes care of inserting results of your Generator into parent syntax correctly.

using System;

namespace A
{
  namespace B
  {
    [CustomGenerator]
    class ToBeProcessed
    {
      public string Text { get; }
    }
  }
}

If your generator returns SyntaxList of class Generated { } you should expect the resulting generated file to contain:

using System;

namespace A
{
  namespace B
  {
    class Generated { }
  }
}

That will also cover nested classes - their "parent" classes will be auto-inserted as well.

mwpowellhtx commented 6 years ago

I don't know, either; at the moment, I am 99.9% confident the generator itself would provide the correct code, unless, let's say, it wasn't before and was silently failing for some reason. This is plausible considering I had to NormalizeWhitespace after all the important bits had been arranged. At any rate, it's helped me improve a couple of things, and raised my awareness of a couple of potentially nasty corner cases re: namespaces. For the typical, sort of, dot delimited namespace, no worries. But for nested ones, or anything more complicated than that, I shall not worry about it for now. Really, it's just more of a concern verifying not only the compilation under test, but also the generated bits aligning with that compilation. The next steps for me are to investigate the connective tissue in more depth, and there are a half dozen or so that deserve a closer look most likely.

amis92 commented 6 years ago

NormalizeWhitespace is also colled by Roslyn.CodeGeneration just before writing to the file anyway, so that was not a problem.

I'd really like to help you out, but without access to your project I don't think I can. Maybe you could prepare a minimal reproduction on GitHub, or sth.

mwpowellhtx commented 6 years ago

@amis92 Well, I'd appreciate it. My project is on Github, for starters. I am in the process of making the transition into Standard/Core. If you are interested you could start there. I am fleshing out my Generator unit tests at the moment. As soon as that is more or less presentable, I will commit that and reconsider the Code Generation integration bits.

mwpowellhtx commented 6 years ago

@amis92 I pushed my work thus far into the repo just now. The next bits for me are to review my comprehension of the Code Generation integration. Otherwise, should be ready for you to have a gander at it if you wouldn't mind.

amis92 commented 5 years ago

Afaik the issue was resolved. Closing.

daiplusplus commented 4 years ago

Just to chime-in (and crosspost from my reply to https://github.com/AArnott/CodeGeneration.Roslyn/issues/186 ) - I'm able to debug and step-through my ICodeGenerator instances by calling System.Diagnostics.Debugger.Launch(); from my ICodeGenerator implementation's class constructor:

public class DuplicateWithSuffixGenerator : ICodeGenerator
{
    private readonly string suffix;

    public DuplicateWithSuffixGenerator( AttributeData attributeData )
    {
        this.suffix = (string)attributeData.ConstructorArguments[0].Value;

        System.Diagnostics.Debugger.Launch();
        while( !System.Diagnostics.Debugger.IsAttached )
        {
            Thread.Sleep( 500 ); // eww, eww, eww
        }
    }

    public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync( TransformationContext context, IProgress<Diagnostic> progress, CancellationToken cancellationToken )
    {
        // Our generator is applied to any class that our attribute is applied to.
        ClassDeclarationSyntax applyToClass = (ClassDeclarationSyntax)context.ProcessingNode;

        // Apply a suffix to the name of a copy of the class.
        ClassDeclarationSyntax copy = applyToClass.WithIdentifier(SyntaxFactory.Identifier(applyToClass.Identifier.ValueText + this.suffix));

        // Return our modified copy. It will be added to the user's project for compilation.
        SyntaxList<MemberDeclarationSyntax> results = SyntaxFactory.SingletonList<MemberDeclarationSyntax>(copy);

        return Task.FromResult( results );
    }
}
dszryan commented 4 years ago

my code generation attribute inherits from the below

    [AttributeUsage(AttributeTargets.Interface)]
    public class GeneratorAttribute : Attribute
    {
        public bool LaunchDebuggerDuringBuild { get; set; }
    }

    public static class GeneratorAttributeExtensions
    {
        internal static void IfRequestedLaunchDebugger(this AttributeData attributeData)
        {
            if (!attributeData.NamedArguments.Any(n => n.Key == nameof(GeneratorAttribute.LaunchDebuggerDuringBuild) && n.Value.ToCSharpString() == "true")) return;
            Debugger.Launch();
            while (!Debugger.IsAttached) Thread.Sleep(500); // eww, eww, eww
        }
    }

N.B.: // eww, eww, eww is still present.

that way at build-runtime I can decide to launch the debugger when I need to investigate something - simply by adding the attribute property 'LaunchDebuggerDuringBuild' when required.