Skip to content
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Expand Down Expand Up @@ -40,13 +41,19 @@ ImmutableArray<ITypeSymbol> enumerations
return;
}

var generatedInterfaceNames = enumerations
.Select(Builder.GetInterfaceNameFor)
.Where(name => name != null)
.Cast<string>()
.ToList();

foreach (var type in enumerations)
{
var typeNamespace = type.ContainingNamespace.IsGlobalNamespace
? $"${Guid.NewGuid()}"
: $"{type.ContainingNamespace}";

var code = Builder.BuildInterfaceFor(type);
var code = Builder.BuildInterfaceFor(type, generatedInterfaceNames);

var hintName = $"{typeNamespace}.I{type.Name}";
context.AddSource(hintName, code);
Expand Down
130 changes: 86 additions & 44 deletions AutomaticInterface/DotnetAutomaticInterface/Builder.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -41,17 +40,37 @@ private static string InheritDoc(ISymbol source) =>
miscellaneousOptions: FullyQualifiedDisplayFormat.MiscellaneousOptions
);

public static string BuildInterfaceFor(ITypeSymbol typeSymbol)
public static string? GetInterfaceNameFor(ITypeSymbol typeSymbol)
{
if (
typeSymbol.DeclaringSyntaxReferences.First().GetSyntax()
is not ClassDeclarationSyntax classSyntax
|| typeSymbol is not INamedTypeSymbol namedTypeSymbol
)
var declarationAndNamedTypeSymbol = GetClassDeclarationMetadata(typeSymbol);
if (declarationAndNamedTypeSymbol == null)
{
return null;
}

var (classSyntax, _) = declarationAndNamedTypeSymbol.Value;

var symbolDetails = GetSymbolDetails(typeSymbol, classSyntax);

return $"global::{symbolDetails.NamespaceName}.{symbolDetails.InterfaceName}";
}

/// <param name="typeSymbol">The symbol from which the interface will be built</param>
/// <param name="generatedInterfaceNames">A list of interface names that will be generated in this session. Used to resolve type references to interfaces that haven't yet been generated</param>
/// <returns></returns>
public static string BuildInterfaceFor(
ITypeSymbol typeSymbol,
List<string> generatedInterfaceNames
)
{
var declarationAndNamedTypeSymbol = GetClassDeclarationMetadata(typeSymbol);
if (declarationAndNamedTypeSymbol == null)
{
return string.Empty;
}

var (classSyntax, namedTypeSymbol) = declarationAndNamedTypeSymbol.Value;

var symbolDetails = GetSymbolDetails(typeSymbol, classSyntax);
var interfaceGenerator = new InterfaceBuilder(
symbolDetails.NamespaceName,
Expand All @@ -60,7 +79,9 @@ is not ClassDeclarationSyntax classSyntax
);

interfaceGenerator.AddClassDocumentation(GetDocumentationForClass(classSyntax));
interfaceGenerator.AddGeneric(GetGeneric(classSyntax, namedTypeSymbol));
interfaceGenerator.AddGeneric(
GetGeneric(classSyntax, namedTypeSymbol, generatedInterfaceNames)
);

var members = typeSymbol
.GetAllMembers()
Expand All @@ -69,15 +90,32 @@ is not ClassDeclarationSyntax classSyntax
.Where(x => !HasIgnoreAttribute(x))
.ToList();

AddPropertiesToInterface(members, interfaceGenerator);
AddMethodsToInterface(members, interfaceGenerator);
AddEventsToInterface(members, interfaceGenerator);
AddPropertiesToInterface(members, interfaceGenerator, generatedInterfaceNames);
AddMethodsToInterface(members, interfaceGenerator, generatedInterfaceNames);
AddEventsToInterface(members, interfaceGenerator, generatedInterfaceNames);

var generatedCode = interfaceGenerator.Build();

return generatedCode;
}

private static (
ClassDeclarationSyntax Syntax,
INamedTypeSymbol NamedTypeSymbol
)? GetClassDeclarationMetadata(ITypeSymbol typeSymbol)
{
if (
typeSymbol.DeclaringSyntaxReferences.First().GetSyntax()
is not ClassDeclarationSyntax classSyntax
|| typeSymbol is not INamedTypeSymbol namedTypeSymbol
)
{
return null;
}

return (classSyntax, namedTypeSymbol);
}

private static GeneratedSymbolDetails GetSymbolDetails(
ITypeSymbol typeSymbol,
ClassDeclarationSyntax classSyntax
Expand All @@ -93,7 +131,11 @@ ClassDeclarationSyntax classSyntax
return new GeneratedSymbolDetails(generationAttribute, typeSymbol, classSyntax);
}

private static void AddMethodsToInterface(List<ISymbol> members, InterfaceBuilder codeGenerator)
private static void AddMethodsToInterface(
List<ISymbol> members,
InterfaceBuilder codeGenerator,
List<string> generatedInterfaceNames
)
{
members
.Where(x => x.Kind == SymbolKind.Method)
Expand All @@ -104,10 +146,14 @@ private static void AddMethodsToInterface(List<ISymbol> members, InterfaceBuilde
.GroupBy(x => x.ToDisplayString(FullyQualifiedDisplayFormatForGrouping))
.Select(g => g.First())
.ToList()
.ForEach(method => AddMethod(codeGenerator, method));
.ForEach(method => AddMethod(codeGenerator, method, generatedInterfaceNames));
}

private static void AddMethod(InterfaceBuilder codeGenerator, IMethodSymbol method)
private static void AddMethod(
InterfaceBuilder codeGenerator,
IMethodSymbol method,
List<string> generatedInterfaceNames
)
{
var returnType = method.ReturnType;
var name = method.Name;
Expand All @@ -116,22 +162,28 @@ private static void AddMethod(InterfaceBuilder codeGenerator, IMethodSymbol meth

var paramResult = new HashSet<string>();
method
.Parameters.Select(p => GetParameterDisplayString(p, codeGenerator.HasNullable))
.Parameters.Select(p =>
p.ToDisplayString(
FullyQualifiedDisplayFormat,
codeGenerator.HasNullable,
generatedInterfaceNames
)
)
.ToList()
.ForEach(x => paramResult.Add(x));

var typedArgs = method
.TypeParameters.Select(arg =>
(
arg.ToDisplayString(FullyQualifiedDisplayFormat),
arg.GetWhereStatement(FullyQualifiedDisplayFormat)
arg.GetWhereStatement(FullyQualifiedDisplayFormat, generatedInterfaceNames)
)
)
.ToList();

codeGenerator.AddMethodToInterface(
name,
returnType.ToDisplayString(FullyQualifiedDisplayFormat),
returnType.ToDisplayString(FullyQualifiedDisplayFormat, generatedInterfaceNames),
InheritDoc(method),
paramResult,
typedArgs
Expand Down Expand Up @@ -187,29 +239,11 @@ private static bool IsNullable(ITypeSymbol typeSymbol)
return false;
}

private static string GetParameterDisplayString(
IParameterSymbol param,
bool nullableContextEnabled
private static void AddEventsToInterface(
List<ISymbol> members,
InterfaceBuilder codeGenerator,
List<string> generatedInterfaceNames
)
{
// If this parameter has default value null and we're enabling the nullable context, we need to force the nullable annotation if there isn't one already
if (
param.HasExplicitDefaultValue
&& param.ExplicitDefaultValue is null
&& param.NullableAnnotation != NullableAnnotation.Annotated
&& param.Type.IsReferenceType
&& nullableContextEnabled
)
{
var addNullable = new AddNullableAnnotationSyntaxRewriter();
return addNullable
.Visit(param.DeclaringSyntaxReferences.First().GetSyntax())
.ToFullString();
}
return param.ToDisplayString(FullyQualifiedDisplayFormat);
}

private static void AddEventsToInterface(List<ISymbol> members, InterfaceBuilder codeGenerator)
{
members
.Where(x => x.Kind == SymbolKind.Event)
Expand All @@ -226,15 +260,16 @@ private static void AddEventsToInterface(List<ISymbol> members, InterfaceBuilder

codeGenerator.AddEventToInterface(
name,
type.ToDisplayString(FullyQualifiedDisplayFormat),
type.ToDisplayString(FullyQualifiedDisplayFormat, generatedInterfaceNames),
InheritDoc(evt)
);
});
}

private static void AddPropertiesToInterface(
List<ISymbol> members,
InterfaceBuilder interfaceGenerator
InterfaceBuilder interfaceGenerator,
List<string> generatedInterfaceNames
)
{
members
Expand All @@ -257,7 +292,7 @@ InterfaceBuilder interfaceGenerator

interfaceGenerator.AddPropertyToInterface(
name,
type.ToDisplayString(FullyQualifiedDisplayFormat),
type.ToDisplayString(FullyQualifiedDisplayFormat, generatedInterfaceNames),
hasGet,
hasSet,
isRef,
Expand Down Expand Up @@ -314,11 +349,18 @@ private static string GetDocumentationForClass(CSharpSyntaxNode classSyntax)
return trivia.ToFullString().Trim();
}

private static string GetGeneric(TypeDeclarationSyntax classSyntax, INamedTypeSymbol typeSymbol)
private static string GetGeneric(
TypeDeclarationSyntax classSyntax,
INamedTypeSymbol typeSymbol,
List<string> generatedInterfaceNames
)
{
var whereStatements = typeSymbol
.TypeParameters.Select(typeParameter =>
typeParameter.GetWhereStatement(FullyQualifiedDisplayFormat)
typeParameter.GetWhereStatement(
FullyQualifiedDisplayFormat,
generatedInterfaceNames
)
)
.Where(constraint => !string.IsNullOrEmpty(constraint));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ namespace DotnetAutomaticInterface;
/// <summary>
/// Source: https://github.com/dominikjeske/Samples/blob/main/SourceGenerators/HomeCenter.SourceGenerators/Extensions/RoslynExtensions.cs
/// </summary>
/// <remarks>
/// Enhancements or additional Roslyn-related extension methods should be placed in <see cref="RoslynExtensionsAutomaticInterface"/>
/// </remarks>
public static class RoslynExtensions
{
private static IEnumerable<ITypeSymbol> GetBaseTypesAndThis(this ITypeSymbol type)
Expand Down
Loading