Roslyn - Extract Interface

RoslynI took it upon myself to write a CodeFix that would save us some time in the team. Specifically we need to extract the interfaces of the classes that comprise the kernel of our system and use these in our DI-system. I wrote a DiagnosticsAnalyzer which compares the interface (if present) with the class, and if there are discrepancies, then report. The Codefix is to loop over all classes and produce one big interfaces.cs file with all the interfaces (yes, generate everything in one file, and yes, I know, but such is the requirement)

I took a brief look at CSharpSyntaxFactory and immediately dismissed it as being too verbose. SyntaxGenerator has everything we need.

var gen = SyntaxGenerator.FromDocument(interfacesfile);

// loop over INamedTypeSymbols and for each call
gen.InterfaceDeclaration( ... ) 

But not so fast ... Let's just have a look at INamedTypeSymbol.GetMembers()

It produces all the members of a Type, which are Properties, Contructors, Fields, Methods and operators. We don't want that.. so we need to filter the members to only get what we need

var methodsForInterface = theClass.GetMembers()
    .OfType<IMethodSymbol>()
    .Where(p => p.MethodKind == MethodKind.Ordinary)
    .Where(p => !p.IsStatic && p.DeclaredAccessibility == Accessibility.Public)
    .ToList();

MethodKind == MethodKind.Ordinay filters out constructs and property-getters and what have you. Also, naturally, only the public instance methods are to be in the interface.

Right.. The result is a List<IMethodSymbol> which is easily enumerated, and SyntaxGenerator has a nice little method 

public Microsoft.CodeAnalysis.SyntaxNode 
MethodDeclaration (Microsoft.CodeAnalysis.IMethodSymbol method, 
System.Collections.Generic.IEnumerable<Microsoft.CodeAnalysis.SyntaxNode> statements = null);

And since we're not interested in the method body we can leave the statements part out. But not all is good. As it turns out, the above method cuts a few corners and leaves out stuff that I want left in. 

 

var mSym = theMethodSymbol;
var synGen = syntaxGenerator;

var methodDecl = synGen.MethodDeclaration(mSym.Name,
    returnType: synGen.TypeExpression(mSym.ReturnType),
    typeParameters: <TODO>
    parameters: mSym.Parameters.Select(p => synGen.ParameterDeclaration(p))
    );

Ok, let's just skip the type-parameters for now and concentrate on the parameters. As stated above, the SyntaxGenerator.ParameterDeclaration(IParameterSymbol) method cuts an important corner as well. It leaves out any potential "Explicit Default values", so if for instance your method looks like this:

public void YourMethod(int parm1, bool parm2 = false, string parm3 = null){ ... }

the interface would end up looking like this:

void YourMethod(int parm1, bool parm2, string parm3)

Again the long way...  

parameters = mSym.Parameters
    .Select(p => synGen.ParameterDeclaration(p.Name,
        type: synGen.TypeExpression(p.Type),
        initializer: p.HasExplicitDefaultValue 
            ? synGen.LiteralExpression(p.ExplicitDetaultValue)
            : null,
        refKind: p.RefKind))

Getting closer... Now, type-parameters.. What if your class is a Generic one? Foo<T>.. We would want our interface to also be generic: IFoo<T>  that's the typeParameters that I left TODO a few lines back. It's actually pretty easy as it's just names, but what if T is constrained to certain types?!

For good measure:

    typeParameters: p.TypeParameters.Select(t => t.Name)

If there are constraints

if(mSym.TypeParameters.Any())
{
    mSym.TypeParameters.Select(t => {
		var specialKind = SpecialTypeConstraintKind.None;
		if(t.HasConstructorConstraint)
			specialKind |= SpecialTypeConstraintKind.Constructor;
		if(t.HasReferenceTypeConstraint)
			specialKind |= SpecialTypeConstraintKind.ReferenceType;
		if(t.HasValueTypeConstraint)
			specialKind |= SpecialTypeConstraintKind.ValueType;
		
		methodDecl = synGen.WithTypeConstraint(methodDecl , t.Name, specialKind, 
			t.ConstraintTypes.Select(ct => gen.TypeExpression(ct)).ToArray());
    }
}

Finally: attributes ... As a lot of VS teams, we still depend on JetBrains Resharper and make use of attributes like [NotNull], [Null], [ItemNotNull] to help our intellisense a bit. Well, attributes are not included in MethodDeclaration.. It's been factored out and you need adorn your methodDecl and parmDecl's yourself.

var attrs = mSym.GetAttributes();
if (attrs.Any())
    methodDecl = synGen.AddAttributes(methodDecl, attrs.Select(a => gen.Attribute(a)));

and likewise within the parameters: mSym.Parameters.Select(...) 

 

Now, the same goes for Public properties.. class.GetMembers().OfType<IPropertySymbol>().Etc().AndSoForth().

I'll leave it to you, to glue everything together. Next up, I'll try to put the generated interface in a file