After my last post talking about source code generators I really wanted to finish off by wiring up an example of the other area I wanted to look into which is Roslyn Analyzers; custom code which allows you to augment the code analysis performed to add your own suggestions and warnings etc.

The reason I feel this is important is that I believe it has the possibility to be a huge help in open source projects. Although tests and documentation are a huge part of helping new contributors make initial changes to your codebase - how much more empowering would it be if they felt confident in their code as they're writing it! Yes you have PR reviews and you can discuss code after the fact, but that's a difficult situation as you have someone who is excited about contributing and you're having to explain consistency after the fact. An analyzer with an appropriate fix could help guide contributors to patterns in your code during the coding so that it's a very passive approach to contributor guidance and ensure the contributor feels more confident when submitting what might be their first change (or - in projects like mine where I'm making a LOT of changes - stop me from needing a contributor to tell me I made yet another obvious mistake ;) )

Initial start

So I've opened up the documentation for my first analyzers and I've done what I always do, which is skim the article to get the gist and then drill into the specifics. Quickly figure out I can't do that as this article is diving directly into "tweak this bit, change this bit" territory. Bit odd. Start reading from the beginning and it straight away tells me to create a project I don't have.

I use VS 2019 - I have web and desktop and VS extensions all installed, nothing. Turns out I needed to add the specific module ".NET Compiler SDK" and then it worked fine.

Next thing - I notice it only gives me .NET Framework options. Okay. Little concerned, source code generator was .NET Core, so I'm getting flashbacks of my VS extension days. I fill in the details and ask for the template..oh yeah, there's a reason the article is giving specifics. Five projects, all filled with code, apparently just to change the casing of a statement. Annnnd there's the vsix to remind me of the times I was bashing my head trying to figure out a VS extension problem. Let's see how it goes.

Generating the diagnostic

Despite my initial reaction to the setup, mostly this is to ensure you can create a solid and compatible diagnostic. And after a little look around the sample I could see why everything was there, even if I wasn't planning to touch it for my proof of concept.

So the first thing is to create the diagnostic - the item that is going to register as a potential issue - and then we can write the fix. My diagnostic is for my JSON library source code generator - so the analytic I wanted to provide a fix for was quite straight forward:

If you write an attribute that is specific to Newtonsoft or System.Text.Json - offer to replace it with the marker attribute alternative.

Should be relatively small, but important for a library wanting to remain agnostic - also quite easily missed by a developer as the attributes are all very similar and familiar, so it might not get picked up until the code review.

The diagnostic in the sample is looking for a symbol, an object name, but actually I'm okay with Syntax - I'm looking for an attribute; so first things first - give the context a method to run when an attribute is discovered

  context.RegisterSyntaxNodeAction(AnalyzeAttribute, SyntaxKind.Attribute);

And then the method that points to raises the diagnostic itself.

private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Info, isEnabledByDefault: true, description: Description);

private void AnalyzeAttribute(SyntaxNodeAnalysisContext context)
{
    var attribute = (AttributeSyntax) context.Node;
    var nameToCheck = attribute.Name.ToFullString();
    if (nameToCheck == "JsonProperty" || nameToCheck == "JsonNameProperty")
    {
        var diagnostic = Diagnostic.Create(Rule, attribute.Name.GetLocation(), attribute.Name.ToFullString());
        context.ReportDiagnostic(diagnostic);
    }
}

Right now I'm keeping it as an info diagnostic as it's all just proof of concept work but if this were a pattern I were particular about trying to keep in a project I would definitely raise it to a warning. I think error is too harsh for this kind of guideline as would stop developers being able to try out possible exceptions or disable their ability to delve into the "why" of the guideline itself.

Also notice I'm not registering the entire attribute for this - just the name. A more complete sample would probably register the syntax so we could look at the attributes and update them each in turn, for just the Json properties I'll keep to a name replacement.

But I got the result I'm after:

A screenshot showing the analyzer showing the diagnostic output

So now I have the diagnostic, I need the fix.

Offering up the fix

So the fix is something that we can attach to the diagnostic. For this example it's to replace it with a JProperty attribute (The marker attribute I created for the source code generator project). The sample has a fixes project which means the code I need to change is easy to find. Now the sample is a really good example of how you can make quite sweeping changes with not too much code, but I really just want a simple text replacement for now.

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
    var diagnostic = context.Diagnostics.First();
    var diagnosticSpan = diagnostic.Location.SourceSpan;

    // Register a code action that will invoke the fix.
    context.RegisterCodeFix(
        CodeAction.Create(
            title: CodeFixResources.CodeFixTitle,
            createChangedDocument: c => MakeAgnostic(context.Document, diagnosticSpan, c),
            equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),
        diagnostic);
}

And then actual code to make the change (no doubt this is overly simplistic, but it shows the concept easily enough)

private Task<Document> MakeAgnostic(Document document, TextSpan span, CancellationToken cancellationToken)
{
    return Task.FromResult(
        document.TryGetText(out var text) ? document.WithText(text.Replace(span, "JProperty")) : document
        );
}

I know I'm okay doing a replace on the span because earlier we received the whole attribute syntax, but only registered the name in the diagnostic, so this span should...yes - it will just replace the name and keep the arguments in place.

A screenshot showing the analyzer fix option and possible output

So from a bit of a shaky start, I feel pretty good about that. Not at all as scary as it first looked, mostly due to the really complete sample project that is generated when you start up. An analyzer and fix to accompany the earlier source code generator. Result!

Next steps?

In terms of next steps for this? Padding out the simple change I was making so it went further would be next, checking project references to highlight if you're still including Newtonsoft would be a good one - allowing for the guidance to help on projects which were actually converting away from specific and toward a more agnostic approach.

This post may seem quite involved, but actually writing this post probably took longer than figuring out how to get the sample working. Of course a real project is going to take some proper time - but with this post and the previous one what I think was most important was that I showed potential!

A big part of the problem with open source is to give developers at any level that confidence to say "I can help" and really believe it. In .NET communities it's often said that hurdle is a little larger, and for whatever reason we know it happens and we miss out on so many talented developers having that feeling of building something that helps others or just helps themselves in some small way.

Tooling is a great passion for me; I always struggle to make things that are pretty or fun but I love building stuff that other developers can use to make things pretty and fun. I still can't explain the buzz I get every time I get a message or see a note on a site that mentions that they produced something with Alexa.NET - that was my first foray into the world of open source contributions and it's been an amazing journey for me.

Now as I start to delve into analyzers and source code generators, and admittedly it's only early days starting to dip a toe into the waters of this tech, I can clearly see potential for IDEs that help make that first contribution a little easier by being a little bit more aware of the context their being used for. Not the larger context of "this language" or "using this syntax" but a more nuanced world where it's not warnings and errors but little nudges that just say "we know this might be new, but would you mind trying it this way just for this project". That faster feedback from a source which developers are more comfortable with could be a huge help to advancing contributions.

And yes, I ramble when I'm excited by an idea :)