Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<EnableNETAnalyzers>True</EnableNETAnalyzers>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
<Version>5.2.6</Version>
<Version>6.0.0</Version>
<PackageReadmeFile>README.md</PackageReadmeFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NoWarn>1701;1702;NU5128</NoWarn>
<PackageReleaseNotes>mproves inheritdoc so that developer documentation is properly referenced on the autogenerated interfaces</PackageReleaseNotes>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='DebugGenerator|AnyCPU'">
<DefineConstants>TRACE;DEBUGGENERATOR</DefineConstants>
Expand Down
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
147 changes: 86 additions & 61 deletions AutomaticInterface/AutomaticInterface/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,46 +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
)
{
var paramParts = param.ToDisplayParts(FullyQualifiedDisplayFormat);
var typeSb = new StringBuilder();
var restSb = new StringBuilder();
var isInsideType = true;
// The part before the first space is the parameter type
foreach (var part in paramParts)
{
if (isInsideType && part.Kind == SymbolDisplayPartKind.Space)
{
isInsideType = false;
}
if (isInsideType)
{
typeSb.Append(part.ToString());
}
else
{
restSb.Append(part.ToString());
}
}
// 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
)
{
typeSb.Append('?');
}
return typeSb.Append(restSb).ToString();
}

private static void AddEventsToInterface(List<ISymbol> members, InterfaceBuilder codeGenerator)
{
members
.Where(x => x.Kind == SymbolKind.Event)
Expand All @@ -243,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 @@ -274,7 +292,7 @@ InterfaceBuilder interfaceGenerator

interfaceGenerator.AddPropertyToInterface(
name,
type.ToDisplayString(FullyQualifiedDisplayFormat),
type.ToDisplayString(FullyQualifiedDisplayFormat, generatedInterfaceNames),
hasGet,
hasSet,
isRef,
Expand Down Expand Up @@ -331,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
Loading
Loading