Introduction and Recap
In my last post, Enforcing Method Parameter Conventions Using Roslyn Diagnostics With C# Part 1:Diagnostics , I showed how you can use the .NET Compiler Platform (Roslyn) to enforce conventions in your code base by implementing code diagnostics.
More specifically, we looked at enforcing method parameter conventions for a given user defined type (or interface). If you want additional insight on the sample convention, please refer to the previous post. To recap, our convention has the following properties.
- Any public service method in the internal service layer must specify a parameter type of IInternalUser.
- The IInternalUser parameter must be specified first in the method signature.
- Only implemented members of service interfaces can be public. Everything else must be private.
Along with these properties are several assumptions that are mentioned in the previous article in order to keep this example short and to the point. The result of the last post left us with a diagnostic that gave us an error or warning if this convention was not followed. An example of this is shown in the screenshot below.
This was a very basic task and provided realistic benefits based on our convention. However, we want to also be able to provide suggested code fixes when this occurs. For this example we want to rearrange the parameters in the method signature to satisfy our convention. This is where the ICodeFixProvider helps.
Using ICodeFixProvider, we can specify instructions for replacing the existing syntax tree with an updated tree which includes our transformed fixes. This is not quite as trivial as implementing our DiagnosticAnalyzer.
We already have implemented our code diagnostic, so we just our code fix. Assuming you have followed the directions correctly in the first article, you should still have your template CodeFixProvider.cs in your project. It should look the same as the following screenshot.
The template code provides a fix that will make declarations all upper case, but we do not care about this. Our goal is to take the improperly ordered parameter list and replace it with one that satisfies our convention.
Step 1: Implement GetFixesAsync
We need to rewrite the logic for GetFixesAsync to find the method declaration(s) that our implemented diagnostic was created for and return a code action for each that will invoke the fix that we implement. This leaves us with a fairly straight forward implementation for GetFixesAsync as the logic for transforming the syntax tree will be housed in our own FixIInternalUserOrdinal which we will implement in the next step.
Go ahead and replace the GetFixesAsync with the following implementation.
I attempted to comment anything that was not straight forward and will not spend time going over each line of this implementation. If you would are having trouble understanding any of this, I would recommend spending a few minutes reviewing Getting-Started – Syntax Transformation (C#).
It is important to understand that we are grabbing the method declaration from the diagnostic that was provided, and then passing it into our FixIInternalUserOrdinal.
Step 2: Implement FixIInternalUserOrdinal
Unlike GetFixesAsync which has no logic for transforming the syntax tree, FixIInternalUserOrdinal is entirely responsible for this transformation and contains the logic for reordering the parameters in our method signature to satisfy our convention.
If you have not done it already, you can delete MakeUppercaseAsync and add the following code.
Again, I am not going to spend time explaining each of these lines of code as I have done my best to explain it through the use of comments.
Instead, I will give a brief summary of the steps I took to reorder the parameters in the method signature. It is important to note that syntax trees are immutable. This means that even if the API exposes methods such as Replace(..) and WithXXX(…), it is actually creating a new object. The summary below uses terminology loosely but you should still keep this in mind.
- Grab a reference to the original parameter list node from the method declaration.
- Filter the list of parameter nodes to those with which use IdentifierNameSyntax so we can search for any tokens named “IInternalUser”.
- Iterate over each parameter in this filtered list until we find one that has a token matching “IInternalUser”.
- Create a new parameter list node and add the IInternalUser parameter node as the first item in the parameter node list.
- Iterate over the rest of the parameters and add to the new parameter list if not the IInternalUser parameter node.
- Grab old syntax tree root and replace the old parameter list node with the new one that was just created with the correct parameter list.
- Return the new document with the transformed syntax tree.
You should now be able to test and even debug your new diagnostic by hittingControl + F5. This should spawn a new Visual Studio Instance in which you can use our previous project example that should use your diagnostic as an extension.
You should see the following diagnostic feedback.
Now that you have implemented a code fix, you should see a light bulb overlay appear in the left margin.
If you click on this, you should see our code fix Reorder Parameters.
The final result should look like the following in which we no longer see any diagnostic feedback because our convention is now satisfied!
Syntax Tree Visualizer
When you download the Roslyn preview, you can also utilize a useful tool within Visual Studio 2013, The Syntax Tree Visualizer. This proved to be very helpful as I was manipulating the syntax trees using transforms. As an example, I have provided a before and after screenshot of our test code following the code fix. If you look closely, you will see the highlighted parameter (which represents our IInternalUser parameter) change positions under the method declaration syntax.
The screenshot below shows the syntax tree before the code fix.
Here is the same syntax tree after our code fix has been executed.
We have implemented a diagnostic and code fix to satisfy our convention. In our next article, I will probably try a more complex example.
I understand that there is several if not dozens of different ways that this example could have been accomplished. If you seem something I could have done more efficiently, or something is done incorrectly, let me know! This is all new to me and I am enjoying getting to know the Roslyn API. I appreciate any feedback that can be given on the subject matter. Additionally, if you have any ideas on conventions that you might have on your development team that you think would be a good example, let me know as well!