I recently had to create a Roslyn Analyzer that envelopes code in a try/catch statement.
This reasoning was to prevent loading errors in any exported MEF component . Here’s the analyzer in action:
The analyzer does the following:- Adds a Diagnostic that finds a MEF ImportingConstructor with content that’s not entirely wrapped in a try/catch statement.
- Provides a Code Fix to handle the problem.
- The Code Fix will:
- Add a try/catch statement around the entire content
- Add ErrorNotificationLogger.LogErrorWithoutShowingErrorNotificationUI(“Error in MEF ctor”, e); inside the catch.
- Add a using statement that for the static class ErrorNotificationLogger
The analyzer code is available on GitHub , but if you’re interested in the explanation, we’ll see how this sort of analyzer can be created.
Getting Started
If you never created Roslyn Analyzers before, you might want to read the getting started tutorial first. If you never worked with Roslyn before, I suggest first starting with Josh Varty’s tutorials .
Start with the regular Roslyn Analyzer template in File | New Project | C# | Extensibility | Analyzer with Code Fix (NuGet + VSIX) template.
Each analyzer consists of a Diagnostic and a CodeFix.
The Diagnostic in our case will find constructors with the ImportingConstructor attribute and mark them as Error.
The CodeFix will wrap the code in the constructor with try/catch.
The Diagnostic
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MefImportExceptionAnalyzerAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "MefImportExceptionAnalyzer";
private static readonly LocalizableString Title = "MEF Import exception Logger";
private static readonly LocalizableString MessageFormat = "There's a MEF ImportingConstructor without a try..catch block .";
private static readonly LocalizableString Description = "All MEF ImportingConstructor should have a try..catch on entire content.";
private const string Category = "MEF";
private static DiagnosticDescriptor Rule =
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat,
Category, DiagnosticSeverity.Error, isEnabledByDefault: true,
description: Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{ get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeConstructor,
ImmutableArray.Create(SyntaxKind.ConstructorDeclaration));
}
private void AnalyzeConstructor(SyntaxNodeAnalysisContext context)
{
var ctor = (ConstructorDeclarationSyntax)context.Node;
bool isDiagnosticNeeded = IsDiagNeeded(ctor);
if (isDiagnosticNeeded)
{
var diag = Diagnostic.Create(Rule, ctor.GetLocation());
context.ReportDiagnostic(diag);
}
}
private bool IsDiagNeeded(ConstructorDeclarationSyntax ctor)
{
bool isAttributeExists = IsImportingAttributeExists(ctor);
if (!isAttributeExists)
return false;
bool isWhiteSpaceOnly = IsWhiteSpaceOnly(ctor);
if (isWhiteSpaceOnly)
return false;
bool tryCatchOnAllExists = IsTryCatchStatementOnly(ctor);
if (tryCatchOnAllExists)
return false;
return true;
}
private static bool IsTryCatchStatementOnly(
ConstructorDeclarationSyntax ctor)
{
var statements = ctor.Body.Statements;
return statements.Count == 1
&& statements[0] is TryStatementSyntax;
}
private static bool IsWhiteSpaceOnly(ConstructorDeclarationSyntax ctor)
{
return ctor.Body.Statements.Count == 0;
}
private static bool IsImportingAttributeExists(
ConstructorDeclarationSyntax ctor)
{
var attrs = ctor.AttributeLists.SelectMany(list => list.Attributes);
return attrs.Any(attr => attr.Name.ToString() == "ImportingConstructor");
}
}
Here’s what happens here:
- In Initialize method we register an Action on any ConstructorDeclaration.
- In AnalyzeConstructor we check 3 conditions:
- if the constructor has the ImportingConstructor attribute. should be true
- If the body of the constructor is white space only . should be false
- If the entire body is wrapped in a single try/catch statement. should be false
- If the 3 conditions are met, then we call ReportDiagnostic.
The Code Fix
The Code Fix is a bit more complicated. We’ll see it in parts (the entire file can be seen here on GitHub)
The code fix starts with:
[Microsoft.CodeAnalysis.CodeFixes.ExportCodeFixProvider(
Microsoft.CodeAnalysis.LanguageNames.CSharp,
Name = nameof(MefImportExceptionAnalyzerCodeFixProvider)), Shared]
public class MefImportExceptionAnalyzerCodeFixProvider : CodeFixProvider
{
private const string title = "Add try.. catch inside";
private const string ERROR_NOTIFICATION_NAMESPACE = "DebuggerShared.Services.ErrorNotification";
private const string SYSTEM_NAMESPACE = "System";
public sealed override ImmutableArray<string> FixableDiagnosticIds
{
get { return
ImmutableArray.Create(MefImportExceptionAnalyzerAnalyzer.DiagnosticId);
}
}
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;
var initialToken = root.FindToken(diagnosticSpan.Start);
var ctor =
FindAncestorOfType<ConstructorDeclarationSyntax>(initialToken.Parent);
context.RegisterCodeFix(
CodeAction.Create(title, c => ChangeBlock(context.Document, ctor, c),
equivalenceKey: title),
diagnostic);
}
private T FindAncestorOfType<T>(SyntaxNode node) where T : SyntaxNode
{
if (node == null)
return null;
if (node is T)
return node as T;
return FindAncestorOfType<T>(node.Parent);
}
Explanation:
- According to FixableDiagnosticIds, the code fix will run only for our specific Diagnostic.
- RegisterCodeFixesAsync will run for each found Diagnostic. It will:
- Find the diagnostic span
- Register a code fix with a createChangedDocument function
c => ChangeBlock(context.Document, ctor, c) where ‘c’ is a CancellationToken
This is it for the boilerplate part of the analyzer. The next part is code manipulation with Roslyn, where we will transform any constructor to be wrapped in a try/catch statement. This will be done in the ChangeBlock method next.
private async Task<Document> ChangeBlock(Document document, ConstructorDeclarationSyntax originalCtor, CancellationToken c)
{
ConstructorDeclarationSyntax newCtor = CreateConstructorWithTryCatch(originalCtor);
var root = await GetRootWithNormalizedConstructor(document, originalCtor, newCtor).ConfigureAwait(false);
root = AddNamespaceIfMissing(root, ERROR_NOTIFICATION_NAMESPACE);
root = AddNamespaceIfMissing(root, SYSTEM_NAMESPACE);
return document.WithSyntaxRoot(root);
}
private static ConstructorDeclarationSyntax CreateConstructorWithTryCatch(ConstructorDeclarationSyntax originalCtor)
{
var originalBlock = originalCtor.Body;
var newCtor = originalCtor.WithBody(
Block(
TryStatement(
SingletonList<CatchClauseSyntax>(
CatchClause()
.WithDeclaration(
CatchDeclaration(
IdentifierName("Exception"))
.WithIdentifier(
Identifier("e")))
.WithBlock(
Block(
SingletonList<StatementSyntax>(
ExpressionStatement(
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("ErrorNotificationLogger"),
IdentifierName("LogErrorWithoutShowingErrorNotificationUI")))
.WithArgumentList(
ArgumentList(
SeparatedList<ArgumentSyntax>(
new SyntaxNodeOrToken[]{
Argument(
LiteralExpression(
SyntaxKind.StringLiteralExpression,
Literal("Error in MEF ctor"))),
Token(SyntaxKind.CommaToken),
Argument(
IdentifierName("e"))})))))))))
.WithBlock(originalBlock))).NormalizeWhitespace();
return newCtor;
}
private static async Task<CompilationUnitSyntax> GetRootWithNormalizedConstructor(Document document, ConstructorDeclarationSyntax originalCtor, ConstructorDeclarationSyntax newCtor)
{
var tree = await document.GetSyntaxTreeAsync().ConfigureAwait(false);
CompilationUnitSyntax root = await tree.GetRootAsync() as CompilationUnitSyntax;
root = root.ReplaceNode(originalCtor, newCtor);
var entirelyNormalizedRoot = root.NormalizeWhitespace();
ConstructorDeclarationSyntax ctorInEntirelyNormalized = FindSpecificConstructor(originalCtor.ParameterList, originalCtor.Identifier.Text, entirelyNormalizedRoot);
var ctorInOrig2 = FindSpecificConstructor(originalCtor.ParameterList, originalCtor.Identifier.Text, root);
ctorInEntirelyNormalized = ctorInEntirelyNormalized.WithParameterList(originalCtor.ParameterList);
ctorInEntirelyNormalized = ctorInEntirelyNormalized.WithAttributeLists(originalCtor.AttributeLists);
var newRoot = root.ReplaceNode(ctorInOrig2, ctorInEntirelyNormalized);
return newRoot;
}
private static ConstructorDeclarationSyntax FindSpecificConstructor(ParameterListSyntax paramList, string identifierText, CompilationUnitSyntax parentNode)
{
var res = parentNode.DescendantNodes().
OfType<ConstructorDeclarationSyntax>()
.SingleOrDefault(c => c.Identifier.Text == identifierText
&& IsParamListEqual(c.ParameterList, paramList)
&& !c.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword)));
return res;
}
private static bool IsParamListEqual(
ParameterListSyntax paramsA, ParameterListSyntax paramsB)
{
if (paramsA == null || paramsB == null)
return false;
var parametersA = paramsA.Parameters;
var parametersB = paramsB.Parameters;
if (parametersA == null
|| parametersB == null
|| parametersA.Count != parametersB.Count)
return false;
for (int i = 0; i < parametersA.Count; i++)
{
var a = Regex.Replace(parametersA[i].ToString(), @"\s+", "");
var b = Regex.Replace(parametersB[i].ToString(), @"\s+", "");
if (a != b)
return false;
}
return true;
}
private CompilationUnitSyntax AddNamespaceIfMissing(
CompilationUnitSyntax root, string namespaceIdentifyer)
{
var ns = root.DescendantNodesAndSelf()
.OfType<UsingDirectiveSyntax>()
.FirstOrDefault(elem => elem.Name.ToString() == namespaceIdentifyer);
if (ns != null)
return root;
var usingDirective =
SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName(namespaceIdentifyer))
.WithTrailingTrivia(SyntaxFactory.EndOfLine("\r\n"));
var lastUsing = root.DescendantNodesAndSelf()
.OfType<UsingDirectiveSyntax>().Last();
root = root.InsertNodesAfter(lastUsing, new[] { usingDirective });
return root;
}
Explanation:
- We create a new constructor with the desired try/catch statement. We can see in the bottom of CreateConstructorWithTryCatch that the Body of try is the body of the previous constructor.
You can easily generate such code with Kiril Osenkov’s Roslyn Quoter . - We replace the old constructor with the new constructor with
var newRoot = root.ReplaceNode(ctorInOrig2, ctorInEntirelyNormalized); - The entire following code is to add using statements. This was needed because for Sysetm.Exception and for my own static class ErrorNotificationLogger.
We basically find the last using statement, and insert a new using statement afterward. - We return the new root (CompilationUnitSyntax)
This is it. As mentioned, the Analyzer code is available on GitHub and it’s also published as a Nuget package OzCode.VisualStudioExtensionAnalyzers .
For more tutorials on Visual Studio Extensibility, you can start with https://michaelscodingspot.com/2017/10/08/visual-studio-2017-extension-development-tutorial-part-1/
For more tutorials on Roslyn, you can start with Josh Varty’s tutorials .