left-icon

Roslyn Succinctly®
by Alessandro Del Sole

Previous
Chapter

of
A
A
A

CHAPTER 5

Writing Refactorings

Writing Refactorings


Refactoring code means reorganizing portions of code a better way, without changing the original behavior. Visual Studio 2015 dramatically enhances the refactoring experience, improving the tooling in C# and introducing support for refactoring to Visual Basic (VB) for the first time. The code refactoring tooling is built upon the .NET Compiler Platform, and as is the case for code analyzers, you can write your own domain-specific refactorings that integrate into light bulbs. In this chapter, you’ll learn how to create a custom refactoring and revisit many techniques you learned in Chapter 4. Remember: the Syntax Visualizer window is always your best friend.

Creating a Code Refactoring

Code refactorings are available when a developer manually enables the light bulb over a piece of code—for example, when right-clicking the code editor and selecting Quick Actions. Available refactorings depend on the kind of the selected syntax node. This is different from code analyzers, which report a warning or an error message by placing squiggles in the code editor and providing information in the Error List window.

You create a code refactoring using the Code Refactoring (VSIX) project template, which is available in the Extensibility node under the programming language of your choice in the New Project dialog (see Figure 34). This is actually the same location of the project template for code analyzers.

Creating a code refactoring project

Figure 34: Creating a code refactoring project

There are many scenarios where code refactorings fit well. For instance, you can rewrite a piece of code to improve maintainability and readability, or you could automate the implementation of an interface within a class definition.

In this chapter, you’ll learn how to create a custom refactoring that targets public fields and checks whether the first letter in an identifier’s declaration is uppercase. If it isn’t, the refactoring provides a quick way to rewrite the field declaration where the first character of its identifiers are uppercase. This is useful for addressing a requirement in the .NET Framework Design Guidelines, which state that identifiers of public members should be named using the Pascal casing notation, where the first character is uppercase.

To start, create a new project using the Code Refactoring (VSIX) template and name it PublicFieldRefactoring (see Figure 34).

Note: Unlike code analyzer projects, the Code Refactoring project template does not generate a NuGet package. Therefore, the only default way to publish a code refactoring is by using a VSIX package. In Chapter 6, “Deploying Analyzers to NuGet,” you’ll learn some tricks for including a code refactoring in a NuGet package.

Structure of Code Refactorings

When you create a code refactoring project, Visual Studio generates two projects:

  • The actual code refactoring project, which is a portable class library. The compiled library will expose the custom code refactoring to development tools like Visual Studio. As with analyzers, this kind of project can contain multiple code refactorings.
  • An extensibility project that generates a VSIX package for deploying the code refactoring library. The generated VSIX package is also deployed to the experimental instance of Visual Studio for debugging purposes. Chapter 7 describes how to share code refactorings with other developers by publishing the VSIX package to the Visual Studio Gallery.

The project contains a sample refactoring that works against type names and offers to reverse characters in the type name. Even though it’s not very useful in real world scenarios, this can certainly be useful for instructional purposes, so it’s worth taking a look at the code. By default, code refactorings are implemented in the CodeRefactoringProvider.cs (or .vb) code file. This is a conventional file name, and can be renamed with a more meaningful name; you will typically provide meaningful file names when you add multiple code refactorings to a single project.

Tip: A code refactoring project can expose an arbitrary number of custom code refactorings. If you want to add other code refactorings to the current project, right-click its name in Solution Explorer, select Add > New Item, and in the Add New Item dialog, select the Refactoring item template available in the Extensibility node.

A code refactoring is a class that inherits from Microsoft.CodeAnalysis.CodeRefactorings.CodeRefactoringProvider, which provides the common refactoring infrastructure. Such a class must be decorated with the ExportCodeRefactoringProvider attribute, which takes two arguments: the language the code refactoring is designed for, and its name. The main entry point for a code refactoring is an asynchronous method called ComputeRefactoringsAsync, which is the place where you implement the refactoring logic. You can examine a code refactoring’s structure by opening the auto-generated CodeRefactoringProvider.cs (or .vb) file and looking at the implementation of the sample refactoring, including comments. In this chapter, you will rewrite the code refactoring from scratch, ignoring the sample offered by Visual Studio.

Understanding Syntax Elements

Before you write a code refactoring that allows replacing the first letter of identifiers in a public field with an uppercase letter, you need to understand which syntax element you need to work with, and this is something you do again with the Syntax Visualizer tool. Visual Basic and C# have different syntax to represent a field declaration, and this is important because the sample code will be slightly different for the two languages.

Figure 35 shows the Syntax Visualizer over a C# field declaration, whereas Figure 36 shows it over a Visual Basic field declaration.

The syntax representation of a field declaration in C#

Figure 35: The syntax representation of a field declaration in C#

The syntax representation of a field declaration in Visual Basic

Figure 36: The syntax representation of a field declaration in Visual Basic

These are the most important considerations at this point:

  • A field declaration is a FieldDeclaration syntax node, mapped by the FieldDeclarationSyntax class in both languages.
  • In C#, FieldDeclarationSyntax exposes a property called Declaration of type VariableDeclarationSyntax, which exposes a property called Variables, a collection of VariableDeclaratorSyntax objects, each representing an identifier declaration in the field.
  • In Visual Basic, FieldDeclarationSyntax exposes a property called Declarators, a collection of VariableDeclaratorSyntax objects, each representing an identifier declaration in the field.

Implementing the Refactoring Logic

In Visual Basic, you need to iterate the FieldDeclarationSyntax.Declarators property and search for any identifier that starts with a lowercase letter; in C#, you need to iterate the FieldDeclarationSyntax.Declaration.Variables property with the same goal. Common to both languages, you need a method that converts the identifier name the appropriate way, and you need to create and register an action, similarly to what you did with code fixes in the previous chapter.

To create an action, you invoke the Microsoft.CodeAnalysis.CodeActions.CodeAction.Create static method (the same used with code fixes) and register the action, invoking the RegisterRefactoring method exposed by the refactoring context instance.

Code Listing 16 shows how to implement the ComputeRefactoringAsync method. Notice that from now on, since the code re-uses many concepts described in Chapter 4, I will not explain these concepts in the text, but detailed comments have been added to the code to aid your understanding.

Code Listing 16 (C#)

        public sealed override async Task

            ComputeRefactoringsAsync(

            CodeRefactoringContext context)

        {

            // Get the root syntax node

            var root = await context.Document.

                GetSyntaxRootAsync(context.CancellationToken).

                ConfigureAwait(false);

            // Find the node at the selection

            var node = root.FindNode(context.Span);

            // Convert the node into a field declaration

            var fieldDecl = node as FieldDeclarationSyntax;

            // If it's not public, return

            if (fieldDecl == null ||

                fieldDecl.Modifiers.ToFullString().

                Contains("public") == false) { return; }

            // Used to determine if an action must

            // be registered

            bool mustRegisterAction = false;

            // If at least one starting character is

            // lowercase, must register an action

            if (fieldDecl.Declaration.

                Variables.Any(i =>

                char.IsLower(i.Identifier.

                ValueText.ToCharArray().First())))

            {

                mustRegisterAction = true;

            }

            else

            {

                mustRegisterAction = false;

            }

           

            if (mustRegisterAction==false)

            {

                return;

            }

            // Create an action invoking a delegate passing

            // the document instance, the syntax node, and

            // a cancellation token

            var action =

                CodeAction.Create("Make first char upper case",

                           c => RenameFieldAsync(context.

                           Document, fieldDecl, c));

            // Register this code action

            context.RegisterRefactoring(action);      

        }

Code Listing 16 (VB)

Public NotOverridable Overrides Async _

        Function ComputeRefactoringsAsync(

                 context As CodeRefactoringContext) As Task

        'Get the root syntax node

        Dim root = Await context.Document.

            GetSyntaxRootAsync(context.

            CancellationToken).ConfigureAwait(False)

        'Find the node at the selection

        Dim node = root.FindNode(context.Span)

        'Convert the node to a field declaration

        Dim fieldDecl = TryCast(node,

            FieldDeclarationSyntax)

        'If it's not public, return

        If fieldDecl Is Nothing Or fieldDecl.Modifiers.

            ToFullString.Contains("Public") = False Then

            Return

        End If

        'Used to determine if an action

        'must be registered

        Dim mustRegisterAction As Boolean

        'Iterate the variable declarators

        For Each declarator

            In fieldDecl.Declarators

            'If at least one starting character

            'is lowercase, must register an action

            If declarator.Names.Any(Function(d) _

                          Char.IsLower(d.Identifier.

                          Value.ToString(0))) Then

                mustRegisterAction = True

            Else

                mustRegisterAction = False

            End If

        Next

        If mustRegisterAction = False Then

            Return

        Else

            'Create an action invoking a delegate passing

            'the document instance, the syntax node, and

            'a cancellation token

            Dim action =

                CodeAction.Create("Make first char upper case",

                           Function(c) RenameFieldAsync(context.

                           Document, fieldDecl, c))

            ' Register this code action

            context.RegisterRefactoring(action)

        End If

    End Function

The next step is to generate a new syntax node for the selected field declaration, supplying updated variable identifiers with the first letter, now uppercase. This is demonstrated in Code Listing 17.

Code Listing 17 (C#)

        private async Task<Document>

            RenameFieldAsync(Document document,

            FieldDeclarationSyntax fieldDeclaration,

            CancellationToken cancellationToken)

        {

            // Get the semantic model for the code file

            var semanticModel =

                await document.

                      GetSemanticModelAsync(cancellationToken).

                      ConfigureAwait(false);

            // Get the root syntax node

            var root = await document.GetSyntaxRootAsync();

            // Get a list of old variable declarations

            var oldDeclarators =

                fieldDeclaration.Declaration.

                Variables;

            // Store the collection of variables

            var listOfNewModifiedDeclarators =

                new SeparatedSyntaxList<

                    VariableDeclaratorSyntax>();

            // Iterate the declarators collection

            foreach (var declarator in oldDeclarators) {

                // Get a new name

                var tempString =

                    ConvertName(declarator.Identifier.

                    ToFullString());

                // Generate a new modified declarator

                // based on the previous one but with

                // a new identifier

                listOfNewModifiedDeclarators =

                    listOfNewModifiedDeclarators.

                    Add(declarator.WithIdentifier(

                        SyntaxFactory.ParseToken(tempString)));

            }

            // Generate a new field declaration

            // with updated variable names

            var newDeclaration =

                fieldDeclaration.Declaration.

                WithVariables(listOfNewModifiedDeclarators);

            // Generate a new FieldDeclarationSyntax

            var newField = fieldDeclaration.

                WithDeclaration(newDeclaration);

            // Replace the old syntax node with the new one

            SyntaxNode newRoot = root.

                       ReplaceNode(fieldDeclaration,

                       newField);

            // Generate a new document

            var newDocument =

                document.WithSyntaxRoot(newRoot);

            // Return the document

            return newDocument;

       }

        // Return a new identifier with an

        // uppercase letter

        private string ConvertName(string oldName)

        {

            return char.

                   ToUpperInvariant(oldName[0]) +

                   oldName.Substring(1);

        }

Code Listing 17 (VB)

    Private Async Function _

        RenameFieldAsync(document As Document,

                         fieldDeclaration As FieldDeclarationSyntax,

                         cancellationToken As CancellationToken) _

                         As Task(Of Document)

        'Get the semantic model for the code file

        Dim semanticModel = Await document.

            GetSemanticModelAsync(cancellationToken).

            ConfigureAwait(False)

        'Get the root syntax node

        Dim root = Await document.GetSyntaxRootAsync

        'Get a list of old declarators

        Dim oldDeclarators =

            fieldDeclaration.Declarators

        'Store the collection of identifiers

        Dim listOfNewModifiedIdentifiers As _

            New SeparatedSyntaxList(

            Of ModifiedIdentifierSyntax)

        'Store the collection of declarators

        Dim listOfNewModifiedDeclarators As _

            New SeparatedSyntaxList(

            Of VariableDeclaratorSyntax)

        'Iterate the declarators collection

        For Each declarator In oldDeclarators

            'For each variable name in the declarator...

            For Each modifiedIdentifier In declarator.Names

                'Get a new name

                Dim tempString =

                    ConvertName(modifiedIdentifier.

                    ToFullString())

                'Generate a new ModifiedIdentifierSyntax based on

                'the previous one's properties but with a new Identifier

                Dim newModifiedIdentifier As _

                    ModifiedIdentifierSyntax =

                    modifiedIdentifier.

                    WithIdentifier(SyntaxFactory.

                    ParseToken(tempString)).

                    WithTrailingTrivia(modifiedIdentifier.

                    GetTrailingTrivia)

                'Add the new element to the collection

                listOfNewModifiedIdentifiers =

                    listOfNewModifiedIdentifiers.

                    Add(newModifiedIdentifier)

            Next

            'Store a new variable declarator with new

            'variable names

            listOfNewModifiedDeclarators =

                listOfNewModifiedDeclarators.Add(declarator.

                WithNames(listOfNewModifiedIdentifiers))

            'Clear the list before next iteration

            listOfNewModifiedIdentifiers = Nothing

            listOfNewModifiedIdentifiers = New _

                SeparatedSyntaxList(Of ModifiedIdentifierSyntax)

        Next

        'Generate a new FieldDeclarationSyntax

        Dim newField = fieldDeclaration.

            WithDeclarators(listOfNewModifiedDeclarators)

        'Replace the old declaration with the new one

        Dim newRoot As SyntaxNode =

            root.ReplaceNode(fieldDeclaration,

                             newField)

        'Generate a new document

        Dim newDocument =

            document.WithSyntaxRoot(newRoot)

        'Return the new document

        Return newDocument

    End Function

    'Return a new identifier with an

    'uppercase letter

    Private Function _

        ConvertName(oldName As String) As String

        Return Char.

            ToUpperInvariant(oldName(0)) &

            oldName.Substring(1)

    End Function

As you can see, you have re-used skills you acquired in Chapter 4 to create new syntax elements for updating a FieldDeclaration syntax node. One new thing you saw was how to create instances of collections that are specific to Roslyn (such as SeparatedSyntaxList) without using the SyntaxFactory class.

Testing and Debugging a Code Refactoring

You test and debug a code refactoring exactly as you do with code analyzers, so you just need to make sure the .vsix project is selected as the startup project, and then press F5. The code refactoring library will be deployed to the experimental instance of Visual Studio, and will be ready for your tests. Of course, you can definitely use the debugging instrumentation you already know. Figure 37 demonstrates how the custom refactoring works over a field declaration in Visual Basic (you get the same result in C#).

Testing a code refactoring

Figure 37: Testing a code refactoring

Custom code refactorings definitely improve the coding experience, not only because they allow reorganizing portions of code a better way, but also because they can simplify the implementation of programming patterns. As an example, imagine you want to offer a refactoring that automates the generation of the RelayCommand and ViewModelBase classes in an implementation of the Model-View-ViewModel pattern. You could create a code refactoring that rewrites the syntax tree of a class by adding the syntax nodes required for the pattern implementation.

Tip: You can take a look at an open-source project I created and published on GitHub, called DotNetAnalyzers. Among others, this project has code refactorings in both VB and C# that automate the generation of classes that implement the MVVM pattern.

Chapter Summary

Refactoring code means reorganizing portions of code a better way without changing the original behavior. In this chapter, you saw how to create a custom code refactoring that integrates with Visual Studio’s quick actions, you saw how to use the Syntax Visualizer to determine the syntax nodes you need to edit, you learned about how a code refactoring is architected, and you tried out the basics for implementing the refactoring logic. Finally, you saw how to test and debug a code refactoring locally. So far, you have learned many concepts about the .NET Compiler Platform, and now you have the skills you need to create code analyzers and refactorings on your own. However, you have only used the result of your work on your own, locally. The big benefit of Roslyn is that code analyzers and refactorings can be shared with other developers. This is discussed in the next two chapters.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.