I am trying to get the hang of Roslyn at the moment, and to implement a couple of code fixes for specific fields/properties that should usually be readonly (in the case of a property, to have no setter).
I've started from the "Analyzer with Code Fix (NUGET + VSIX)" template and now have two diagnostics implemented in the DiagnosticAnalyzer
class and two corresponding code fixes in the CodeFixProvider
class. But in this second class, it all feels a bit "messy" - so I'm looking for feedback on what I can do to clean it up:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace LiveVariables.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LiveVariablesAnalyzersCodeFixProvider)), Shared]
public class LiveVariablesAnalyzersCodeFixProvider : CodeFixProvider
{
private const string readonlyAction = "Make readonly";
public sealed override ImmutableArray<string> FixableDiagnosticIds
{
get { return ImmutableArray.Create(LiveVariablesAnalyzersAnalyzer.LV1001DiagnosticId, LiveVariablesAnalyzersAnalyzer.LV1002DiagnosticId); }
}
public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;
if (diagnostic.Id == LiveVariablesAnalyzersAnalyzer.LV1001DiagnosticId)
{
// Find the field declaration identified by the diagnostic.
var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<FieldDeclarationSyntax>().First();
context.RegisterCodeFix(
CodeAction.Create(
title: readonlyAction,
createChangedDocument: c => MakeFieldReadOnly(context.Document, declaration, c),
equivalenceKey: readonlyAction),
diagnostic);
}
else
{
// Find the property declaration identified by the diagnostic.
var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<PropertyDeclarationSyntax>().First();
context.RegisterCodeFix(
CodeAction.Create(
title: readonlyAction,
createChangedDocument: c => MakePropertyReadOnly(context.Document, declaration, c),
equivalenceKey: readonlyAction),
diagnostic);
}
}
private async Task<Document> MakeFieldReadOnly(Document document, FieldDeclarationSyntax fieldDecl, CancellationToken cancellationToken)
{
var genny = SyntaxGenerator.GetGenerator(document);
var nFieldDecl = genny.WithModifiers(fieldDecl, genny.GetModifiers(fieldDecl) | DeclarationModifiers.ReadOnly);
var newSyntax = (await document.GetSyntaxRootAsync()).ReplaceNode(fieldDecl, nFieldDecl);
return document.WithSyntaxRoot(newSyntax);
}
private async Task<Document> MakePropertyReadOnly(Document document, PropertyDeclarationSyntax propDecl, CancellationToken cancellationToken)
{
var genny = SyntaxGenerator.GetGenerator(document);
var setter = genny.GetAccessor(propDecl, DeclarationKind.SetAccessor);
var newSyntax = (await document.GetSyntaxRootAsync()).RemoveNode(setter, SyntaxRemoveOptions.KeepNoTrivia);
return document.WithSyntaxRoot(newSyntax);
}
}
}
Specific things that feel messy to me:
- Should I even be implementing multiple code fixes in a single
CodeFixProvider
? - the API definitely supports it but it feels it could easily get out of hand - I'm working out what diagnostic I'm responding to, inside
RegisterCodeFixesAsync
by doing a string comparison - is there a stronger-typed way of working this out? - Creating the
SyntaxGenerator
within theMakeFieldReadOnly
andMakePropertyReadOnly
feels a bit wrong somehow - is there a more natural way for me to make my changes?
And of course, anything else people would care to suggest will be interesting.
(I'm generally happy with the code within DiagnosticAnalyzer
but can add it here also if people think that it's important to see all of the code within the project)