Search code examples
c#visual-studio-2017roslynroslyn-code-analysis

Roslyn 2.x CodeFix that implements a missing interface, delegated to a member, VS 2017


BACKGROUND

I am seeking to create a Roslyn CodeFix that will respond to a Diagnostic warning from the built in Analyzer shipped with Visual Studio, that will identify the interface that is not - or is partially - implemented, allowing me to loop through the missing members, and generate custom code that delegates a method call to a Member field of a type that implements the interface.

(The Roslyn Analysers and CodeFixes that ship with Visual Studio do offer this functionality, but I need to customise and extend the generation of code, which is not possible as the Microsoft implementations are all marked as internal.)

Please note: The interface is almost always going to be located in an external, third-party assembly, to which I do not have access to the source.

e.g. starting from:

public class CustomDocumentDBClient : IDocumentClient
{
}

The desired outcome would be similar to the following (in practice I will be creating multiple versions that add additional code to wrap the method calls, once the basic principals are working):

public class CustomDocumentDBClient : IDocumentClient
{
    // Field to which calls are delegated, initialised by the constructor
    private readonly IDocumentClient _documentClientImplementation;

    // Constructor
    public CustomDocumentDBClient (IDocumentClient documentClientImplementation)
    {
        _documentClientImplementation = documentClientImplementation;
    }

    // Interface method that delegates to private field for invocation 
    public Task<ResourceResponse<Attachment>> CreateAttachmentAsync(string documentLink, object attachment, RequestOptions options = null)
    {
        return _documentClientImplementation.CreateAttachmentAsync(documentLink, attachment, options);
    }

    // Interface method that delegates to private field for invocation    
    public Task<ResourceResponse<Attachment>> CreateAttachmentAsync(string documentLink, Stream mediaStream, MediaOptions options = null, RequestOptions requestOptions = null)
    {
        return _documentClientImplementation.CreateAttachmentAsync(documentLink, mediaStream, options, requestOptions);
    }
    ...
    other methods
    ...
}

WHAT I HAVE TRIED

I have spent some time reading tutorials regarding the Syntax Tree and Semantic Model functionality of Roslyn.

I have also examined the Roslyn source code from GitHub - which does include the exact feature that I wish to implement; however, the code is heavily interwoven throughout various complex classes, and is implemented as internal methods, which cannot be overridden or extended, or indeed extracted into a standalone project.

From investigating multiple samples, and also a related SO question How to see if a class has implemented the interface with Roslyn I have concluded that I must use the Roslyn Semantic Model to obtain information about the interface, and it's declared members.

Once I can obtain the interface Members, I am able to build the required output code using the SyntaxFactory, and I have used the 'Roslyn Quoter' for guidance.

Creating a CodeFix from the default template, that responds to the correct Diagnostic codes is straightforward, and this is functioning.

ISSUES

The problem I am having, is taking the token identified by the Diagnostics Location, which appears to be a SimpleBaseTypeSyntax token, and

  1. Verifying that it is actually representing an interface,
  2. Obtaining a Symbol definition that allows me to enumerate the Members of the third-party interface.

The Syntax Visualizer indicates that the interface declaration node is of type SimpleBaseType.

I am therefore using the following code to obtain the token from the Syntax Tree as SimpleBaseTypeSyntax-

    // Find the interface Syntax Token detected by the diagnostic.
    var interfaceBaseTypeSyntax =
        root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf()
            .OfType<SimpleBaseTypeSyntax>().First();

a) This does return a token that contains the information of the relevant node in the Syntax Tree - however, I cannot see any 'InterfaceTypeSyntax' type or IsInterface method to validate that it is actually an interface.

b) I believe I should be able to use semanticModel.GetSymbolInfo(interfaceBaseTypeSyntax), however this always returns null - bear in mind the interface is declared in an external assembly.

Is there something I need to do to make that information available through GetSymbolInfo, or is there another approach I should be taking...?

Many thanks for your advice...


Solution

  • It's rather embarrassing to have found this so quickly after posting, but the solution seems to be to refer to the Identifier which is a descendant of the SimpleBaseTypeSyntax.

    var interfaceSymbolInfo =
    semanticModel.GetSymbolInfo(interfaceBaseTypeSyntax.DescendantNodes().First());
    

    And, by calling:

    var interfaceTypeInfo = 
    semanticModel.GetTypeInfo(interfaceBaseTypeSyntax.DescendantNodes().First());
    

    I can then use interfaceTypeInfo.Type.IsInterface to verify I have indeed found an interface type, and also access interfaceTypeInfo.Type.GetMembers().

    The answer was staring me in the face via the Syntax Visualizer.

    Syntax Visualizer

    I'm leaving this open for now, in case others have a better solution... thanks!