Concepts referred to in this document which are not assumed to be familiar for all readers are explained in the following table.
Name | Description |
---|---|
Analyzer | Analyzers are pieces of code that inspect your C# or Visual Basic code for style, quality, maintainability, and other issues. An analyzer contains code that recognizes violations of rules that you define. |
Code Fixer | A Code Fixer contains the code that fixes the violation by modifying source code. Code fixes in Visual Studio appear as "light bulb" suggestions. |
Language agnostic | We use this phrase to describe when the Analyzer or Code Fixer is able to operate on both C# and Visual Basic. |
We use Roslyn Analyzers to highlight areas of code that will need to be refactored. By default, we aim to pair these Analyzers with Code Fixers to automate as much of the upgrade workflow as possible.
Examples of changes that can be performed include:
- Mapping from one type to another. A common example is System.Web.Http.ApiController should become Microsoft.AspNetCore.Mvc.ControllerBase
- Extracting
HttpContext
via method dependency injection - Replacing methods that are not available on .NET latest such as BinaryFormatter.UnsafeDeserialize
The following guidelines are a list of best practices that we use to guide the development of Analyzers and Code Fixer to support our goals.
- Write language agnostic Analyzers with as much code reuse as possible.
- Analyzers should be as performant as possible.
- Samples included follow best practices as setup by Roslyn Analyzers.
- Use abstractions, and clean code principles, to write code that everyone can read regardless of their Roslyn experience to promote community contributions and reduce code maintenance.
Separation of Analyzer and Code Fixer tests increases complexity and code duplication and tends to decrease the overall confidence in the test suite. If you're testing entire files at a time, you will either be quickly overwhelmed by the number of files per test scenario or be tempted to put multiple test scenarios into a single file which shifts from unit to integration testing. The Microsoft.CodeAnalysis.Testing framework addresses these concerns.
Do
- Read the testing overview: Microsoft.CodeAnalysis.Testing
The Analyzer, or Code Fixer, will be instantiated as a singleton and executed concurrently. Design your DiagnosticAnalyzer
and CodeFixProvider
so that execution can start processing a 2nd call before processing finishes for the 1st call.
The following is an example that illustrates the problem.
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MyAnalyzer : DiagnosticAnalyzer
{
public string MyData { get; set; } //this property is not handled with thread safety
///... override of SupportedDiagnostics omitted for brevity
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeMemberAccessExpressions, SyntaxKind.SimpleMemberAccessExpression);
}
private void AnalyzeMemberAccessExpressions(SyntaxNodeAnalysisContext context)
{
MyData = context.Node.ToString();
if (MyData == "HttpContext")
{
context.ReportDiagnostic(diagnostic);
}
}
}
Only one instance of
MyAnalyzer
will be created andAnalyzeMemberAccessExpressions
will be invoked repeatedly. This will result in a race condition that can lead to inconsistent behavior.
Do
- Use local variables or method arguments to store information.
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class MyAnalyzer : DiagnosticAnalyzer
{
internal static readonly DiagnosticDescriptor Rule = new("EX00001", "Example 1", "Message 1", "Example Category", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Example description.");
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(Option1, SyntaxKind.SimpleMemberAccessExpression);
// in this example: option2 will create a unique MyAnalysisClass object each time a roslyn analysis session is started
context.RegisterSyntaxNodeAction(new MyAnalysisClass("HttpContext").Option2, SyntaxKind.SimpleMemberAccessExpression);
}
private void Option1(SyntaxNodeAnalysisContext context)
{
// use local variables and pass data as method arguments
var myData = context.Node.ToString();
if (myData == "HttpContext")
{
var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
public class MyAnalysisClass
{
private readonly string _myStatefulInfo;
public Analysis(string stateInfo)
{
_myStatefulInfo = stateInfo;
}
internal void Option2(SyntaxNodeAnalysisContext context)
{
if (context.Node.ToString() == _myStatefulInfo)
{
var diagnostic = Diagnostic.Create(MyAnalyzer.Rule, context.Node.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
Note that option2 enables you to organize your code and even do code analysis from classes in .net standard libraries.
Roslyn is a rich framework of information that describes every detail of code in every file of every project. The concepts can become overwhelming. Use abstractions to develop class names and methods that sharpen the focus on what the Analyzer does by hiding how it achieves the goal.
Do
- Use extension methods, and wrapper objects to describe "what" the code does instead of "how" the code behaves.
Roslyn sees class files as rich trees of information. There will likely be millions of syntax nodes to evaluate across a large solution. You should limit the performance impact of running the Analyzer by reducing the number of operations performed.
Do
- Use the Syntax Visualizer to build syntax. Where possible use specific syntax options. As an example, if you are building an Analyzer that will examine class inheritance then you should use
SyntaxKind.BaseList
, which will occur much less often than theSyntaxKind.IdentifierName
. - Use information already available from
SyntaxNodeAnalysisContext
to quickly filter relevant information. As an example, if you build an analyzer that looks for a method then you would be evaluatingMemberAccessExpressionSyntax
nodes. In this scenario, you should also check the parent of the node to figure out if this Syntax is a method or a property.
Build your Analyzer with the expectation that it will be invoked a million times. Look for, and replace, strings to prevent excessive garbage collection due to frequent executions of your Analyzer.
Do
- Use string constants when evaluating string conditionals.
- Cache values from string interpolation, concatenation, or
string.Format
so that new objects are not constructed each time the Analyzer is run.
Enable concurrent execution of your Analyzers and prevent them from running in auto generated code.
Do
- Use the following in your diagnostic Analyzer class.
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
...
}
If you track multiple syntax nodes in your Analyzer then you can make it easier to write your Code Fixer by using the method overload of Diagnostic.Create
that supports additionalLocations.
When searching for patterns of code it is common to perform string comparisons. These approaches can work successfully but you should beware that trivia, the blank space we use to make code more readable, can vary from team to team. Not everyone puts a space after an assignment operator and if there are two spaces, or a tab, after the assignment operator that code is still valid.
Should Not
- Try to avoid
ToFullString
and solutions that require awareness of trivia.
This is a better communication to the end-user when running in Visual Studio and it makes it easier to write the Code Fixer.
If the only methods available are async, you should find another way to implement your Analyzer to prevent runtime issues.
Do Not
- Use
.Result
orWait()
in a Roslyn Analyzer. Forcing asynchronous code to behave synchronously can result in timing issues that are hard to debug and thread starvation.
You may need to do a few final checks before registering your Code Fixer by calling RegisterCodeFix
but you should lean into doing as many checks as possible in the Analyzer.
You can also accelerate your Code Fixer productivity by using the additionalLocations
overload when reporting a diagnostic so that you can find multiple interesting locations in a document when working from a single diagnostic.
In many scenarios, your Code Fixer can apply to C# and Visual Basic. The SyntaxGenerator
is a language agnostic factory for creating syntax nodes.
The trees generated by this API will try to respect user preferences when possible. For example, generating MemberAccessExpression(SyntaxNode, String) will be done in a way such that this.
or Me.
will be simplified according to user preference if any ReduceAsync(Document, OptionSet, CancellationToken)
overload is called.
When replacing a SyntaxNode consider whether the node you're replacing had trivia. Trivia, the blank space we use to make code more readable, can vary from team to team. Not everyone puts a space after an assignment operator and if there are two spaces, or a tab, after the assignment operator then the Code Fixer should respect the file's original formatting.
Do
- Use the
WithLeadingTrivia
andWithTrailingTrivia
extension methods to preserve trivia when creating new nodes.
Code Fixers are asynchronous. Follow best practice guidance for asynchronous programming to prevent race conditions and performance issues.
Learn more about Async best practices by reading Async/Await - Best Practices in Asynchronous Programming by Stephen Cleary
Do
- Leverage the CancellationToken to interrupt processing when cancellation has been requested.
Do Not
- Do not force async code to run synchronously. Avoid calling
.Result
and prefer to use await as needed when working in the Code Fixer construct.
When writing your Code Fixer you need to create a CodeAction
to resolve the diagnostic. If your Code Fixer only changes a single document, then your CodeAction
should return a document.
Should
- Prefer
Task<Document>
when possible and useTask<Solution>
as needed when returning a result from a Code Fixer.
When you need to make many changes, you will want to batch those changes with the SolutionEditor, or DocEditor. It can be helpful to think about these objects as you would think about using StringBuilder when concatenating many strings. These objects enable you to create a cumulative list of changes that can reduce GC pressure created by changing immutable syntax trees.
If you’re not batching changes, then most code changes can be carried out by working directly with the document’s syntax root and the SyntaxGenerator
.
Don’t force users to fix instances of a diagnostic one-by-one. A FixAll occurrences Code Fixer means: I have a Code Fixer 'C', that fixes a specific instance of diagnostic 'D' in my source, and I want to apply this fix to all instances of 'D' across a broader scope, such as a document or a project or the entire solution.
Your Code Fixer should override GetFixAllProvider
to return a non-null instance of FixAll Provider.
Code Fixers are applied as a one-step change and their order is not guaranteed. If the code you’re adding requires a namespace import, then your Code Fixer should perform that change if necessary. Do not assume that another Code Fixer will handle adding the correct namespace.
There are many stopping points on the journey from .NET Framework to .NET latest and even more scenarios specific to our customers. Because of this, upgrade-assistant
does not always assume that the code can be compiled. This section highlights some considerations specific to the "work in progress" state that all projects will pass through as they are upgraded.
upgrade-assistant
will make sweeping changes across the solution. These sweeping changes often result in manual changes that must be made after running upgrade assistant before the code will compile.
As an example, upgrade-assistant
will upgrade NuGet packages across major versions. upgrade-assistant
does not evaluate if the newer package contains breaking changes.
upgrade-assistant
will add, upgrade, and remove package references during the upgrade process. If your Analyzer is looking for a specific symbol then you may need to AddMetadataReference to ensure the symbol is available.
Reference assemblies are a special type of assembly that have only the smallest amount of metadata needed to represent the library's public API surface.
If you need to AddMetadataReference then use a reference assembly when possible.
Examples include:
- Microsoft .NET Framework Reference Assemblies .NET 4.8
- Microsoft .NET Framework Reference Assemblies .NET 4.7.2
https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/syntax-visualizer
The Syntax Visualizer is a tool window that helps you inspect and explore syntax trees. It's an essential tool to understand the models for code you want to analyze. It's also a debugging aid when you develop your own applications using the .NET Compiler Platform (“Roslyn”) SDK.
- YouTube: Learn Roslyn Now by Josh Varty