From a75115e626a7033036a3e62b4a1add73e453dd36 Mon Sep 17 00:00:00 2001
From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com>
Date: Wed, 3 Sep 2025 13:52:55 +1000
Subject: [PATCH 1/8] Fully qualify references to generated interfaces
This addresses an issue where, when a class references a generated interface, those references are not fully qualified, resulting in an interface that doesn't compile.
The change is to precalculate the list of interface names that will be generated, and, during symbol string generation, replace unrecognised symbols with generated names _where a single unambiguous match can be made between the symbol and the list of interfaces being generated_.
---
.../AutomaticInterface/RoslynExtensions.cs | 192 ++++++++++++++++++
.../AutomaticInterfaceGenerator.cs | 9 +-
.../DotnetAutomaticInterface/Builder.cs | 75 +++++--
AutomaticInterface/Tests/Infrastructure.cs | 3 +-
...thods.WorksWithGenericMethods.verified.txt | 2 +-
...tedGenericInterfaceReferences.verified.txt | 41 ++++
...hGeneratedInterfaceReferences.verified.txt | 41 ++++
.../Tests/TypeResolutions/TypeResolutions.cs | 65 ++++++
8 files changed, 409 insertions(+), 19 deletions(-)
create mode 100644 AutomaticInterface/AutomaticInterface/RoslynExtensions.cs
create mode 100644 AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedGenericInterfaceReferences.verified.txt
create mode 100644 AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedInterfaceReferences.verified.txt
diff --git a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs
new file mode 100644
index 0000000..7063a69
--- /dev/null
+++ b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs
@@ -0,0 +1,192 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace AutomaticInterface
+{
+ ///
+ /// Source: https://github.com/dominikjeske/Samples/blob/main/SourceGenerators/HomeCenter.SourceGenerators/Extensions/RoslynExtensions.cs
+ ///
+ public static class RoslynExtensions
+ {
+ private static IEnumerable GetBaseTypesAndThis(this ITypeSymbol type)
+ {
+ var current = type;
+ while (current != null)
+ {
+ yield return current;
+ current = current.BaseType;
+ }
+ }
+
+ public static IEnumerable GetAllMembers(this ITypeSymbol type)
+ {
+ return type.GetBaseTypesAndThis().SelectMany(n => n.GetMembers());
+ }
+
+ public static string GetClassName(this ClassDeclarationSyntax proxy)
+ {
+ return proxy.Identifier.Text;
+ }
+
+ ///
+ /// Thanks to https://www.codeproject.com/Articles/871704/Roslyn-Code-Analysis-in-Easy-Samples-Part-2
+ ///
+ public static string GetWhereStatement(
+ this ITypeParameterSymbol typeParameterSymbol,
+ SymbolDisplayFormat typeDisplayFormat,
+ List generatedInterfaceNames
+ )
+ {
+ var result = $"where {typeParameterSymbol.Name} : ";
+
+ var constraints = new List();
+
+ if (typeParameterSymbol.HasReferenceTypeConstraint)
+ {
+ constraints.Add("class");
+ }
+
+ if (typeParameterSymbol.HasValueTypeConstraint)
+ {
+ constraints.Add("struct");
+ }
+
+ if (typeParameterSymbol.HasNotNullConstraint)
+ {
+ constraints.Add("notnull");
+ }
+
+ constraints.AddRange(
+ typeParameterSymbol.ConstraintTypes.Select(t =>
+ t.ToDisplayString(typeDisplayFormat, generatedInterfaceNames)
+ )
+ );
+
+ // The new() constraint must be last
+ if (typeParameterSymbol.HasConstructorConstraint)
+ {
+ constraints.Add("new()");
+ }
+
+ if (constraints.Count == 0)
+ {
+ return "";
+ }
+
+ result += string.Join(", ", constraints);
+
+ return result;
+ }
+
+ public static string ToDisplayString(
+ this IParameterSymbol symbol,
+ SymbolDisplayFormat displayFormat,
+ List generatedInterfaceNames
+ )
+ {
+ var parameterDisplayString = symbol.ToDisplayString(displayFormat);
+
+ var parameterTypeDisplayString = symbol.Type.ToDisplayString(
+ displayFormat,
+ generatedInterfaceNames
+ );
+
+ // Replace the type part of the parameter definition - we don't try to generate the whole parameter definition
+ // as it's quite complex - we leave that to Roslyn.
+ return ParameterTypeMatcher.Replace(parameterDisplayString, parameterTypeDisplayString);
+ }
+
+ ///
+ /// Matches the type part of a parameter definition (Type name[ = defaultValue])
+ ///
+ private static readonly Regex ParameterTypeMatcher =
+ new(@"[^\s=]+(?=\s\S+(\s?=\s?\S+)?$)", RegexOptions.Compiled);
+
+ ///
+ /// Wraps with custom resolution for generated types
+ ///
+ ///
+ public static string ToDisplayString(
+ this ITypeSymbol symbol,
+ SymbolDisplayFormat displayFormat,
+ List generatedInterfaceNames
+ )
+ {
+ var builder = new StringBuilder();
+
+ AppendTypeSymbolDisplayString(symbol, displayFormat, generatedInterfaceNames, builder);
+
+ return builder.ToString();
+ }
+
+ private static void AppendTypeSymbolDisplayString(
+ ITypeSymbol typeSymbol,
+ SymbolDisplayFormat displayFormat,
+ List generatedInterfaceNames,
+ StringBuilder builder
+ )
+ {
+ if (typeSymbol is not IErrorTypeSymbol errorTypeSymbol)
+ {
+ // This symbol contains no unresolved types. Fall back to the default generation provided by Roslyn
+ builder.Append(typeSymbol.ToDisplayString(displayFormat));
+ return;
+ }
+
+ var symbolName =
+ InferGeneratedInterfaceName(errorTypeSymbol, generatedInterfaceNames)
+ ?? errorTypeSymbol.Name;
+
+ builder.Append(symbolName);
+
+ if (errorTypeSymbol.IsGenericType)
+ {
+ builder.Append('<');
+
+ bool isFirstTypeArgument = true;
+ foreach (var typeArgument in errorTypeSymbol.TypeArguments)
+ {
+ if (!isFirstTypeArgument)
+ {
+ builder.Append(", ");
+ }
+
+ AppendTypeSymbolDisplayString(
+ typeArgument,
+ displayFormat,
+ generatedInterfaceNames,
+ builder
+ );
+
+ isFirstTypeArgument = false;
+ }
+
+ builder.Append('>');
+ }
+ }
+
+ private static string? InferGeneratedInterfaceName(
+ IErrorTypeSymbol unrecognisedSymbol,
+ List generatedInterfaceNames
+ )
+ {
+ var matches = generatedInterfaceNames
+ .Where(name => Regex.IsMatch(name, $"[.:]{unrecognisedSymbol.Name}$"))
+ .ToList();
+
+ if (matches.Count != 1)
+ {
+ // Either there's no match or an ambiguous match - we can't safely infer the interface name.
+ // This is very much a "best effort" approach - if there are two interfaces with the same name,
+ // there's no obvious way to work out which one the symbol is referring to.
+ return null;
+ }
+
+ return matches[0];
+ }
+ }
+}
diff --git a/AutomaticInterface/DotnetAutomaticInterface/AutomaticInterfaceGenerator.cs b/AutomaticInterface/DotnetAutomaticInterface/AutomaticInterfaceGenerator.cs
index b7109e3..ee0f563 100644
--- a/AutomaticInterface/DotnetAutomaticInterface/AutomaticInterfaceGenerator.cs
+++ b/AutomaticInterface/DotnetAutomaticInterface/AutomaticInterfaceGenerator.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Immutable;
+using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -40,13 +41,19 @@ ImmutableArray enumerations
return;
}
+ var generatedInterfaceNames = enumerations
+ .Select(Builder.GetInterfaceNameFor)
+ .Where(name => name != null)
+ .Cast()
+ .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);
diff --git a/AutomaticInterface/DotnetAutomaticInterface/Builder.cs b/AutomaticInterface/DotnetAutomaticInterface/Builder.cs
index 8ad7296..a24554b 100644
--- a/AutomaticInterface/DotnetAutomaticInterface/Builder.cs
+++ b/AutomaticInterface/DotnetAutomaticInterface/Builder.cs
@@ -41,7 +41,28 @@ 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
+ )
+ {
+ return null;
+ }
+ var symbolDetails = GetSymbolDetails(typeSymbol, classSyntax);
+
+ return $"global::{symbolDetails.NamespaceName}.{symbolDetails.InterfaceName}";
+ }
+
+ /// The symbol from which the interface will be built
+ /// 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
+ ///
+ public static string BuildInterfaceFor(
+ ITypeSymbol typeSymbol,
+ List generatedInterfaceNames
+ )
{
if (
typeSymbol.DeclaringSyntaxReferences.First().GetSyntax()
@@ -60,7 +81,9 @@ is not ClassDeclarationSyntax classSyntax
);
interfaceGenerator.AddClassDocumentation(GetDocumentationForClass(classSyntax));
- interfaceGenerator.AddGeneric(GetGeneric(classSyntax, namedTypeSymbol));
+ interfaceGenerator.AddGeneric(
+ GetGeneric(classSyntax, namedTypeSymbol, generatedInterfaceNames)
+ );
var members = typeSymbol
.GetAllMembers()
@@ -69,9 +92,9 @@ 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();
@@ -93,7 +116,11 @@ ClassDeclarationSyntax classSyntax
return new GeneratedSymbolDetails(generationAttribute, typeSymbol, classSyntax);
}
- private static void AddMethodsToInterface(List members, InterfaceBuilder codeGenerator)
+ private static void AddMethodsToInterface(
+ List members,
+ InterfaceBuilder codeGenerator,
+ List generatedInterfaceNames
+ )
{
members
.Where(x => x.Kind == SymbolKind.Method)
@@ -104,10 +131,14 @@ private static void AddMethodsToInterface(List 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 generatedInterfaceNames
+ )
{
var returnType = method.ReturnType;
var name = method.Name;
@@ -124,14 +155,14 @@ private static void AddMethod(InterfaceBuilder codeGenerator, IMethodSymbol meth
.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
@@ -209,7 +240,11 @@ bool nullableContextEnabled
return param.ToDisplayString(FullyQualifiedDisplayFormat);
}
- private static void AddEventsToInterface(List members, InterfaceBuilder codeGenerator)
+ private static void AddEventsToInterface(
+ List members,
+ InterfaceBuilder codeGenerator,
+ List generatedInterfaceNames
+ )
{
members
.Where(x => x.Kind == SymbolKind.Event)
@@ -226,7 +261,7 @@ private static void AddEventsToInterface(List members, InterfaceBuilder
codeGenerator.AddEventToInterface(
name,
- type.ToDisplayString(FullyQualifiedDisplayFormat),
+ type.ToDisplayString(FullyQualifiedDisplayFormat, generatedInterfaceNames),
InheritDoc(evt)
);
});
@@ -234,7 +269,8 @@ private static void AddEventsToInterface(List members, InterfaceBuilder
private static void AddPropertiesToInterface(
List members,
- InterfaceBuilder interfaceGenerator
+ InterfaceBuilder interfaceGenerator,
+ List generatedInterfaceNames
)
{
members
@@ -257,7 +293,7 @@ InterfaceBuilder interfaceGenerator
interfaceGenerator.AddPropertyToInterface(
name,
- type.ToDisplayString(FullyQualifiedDisplayFormat),
+ type.ToDisplayString(FullyQualifiedDisplayFormat, generatedInterfaceNames),
hasGet,
hasSet,
isRef,
@@ -314,11 +350,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 generatedInterfaceNames
+ )
{
var whereStatements = typeSymbol
.TypeParameters.Select(typeParameter =>
- typeParameter.GetWhereStatement(FullyQualifiedDisplayFormat)
+ typeParameter.GetWhereStatement(
+ FullyQualifiedDisplayFormat,
+ generatedInterfaceNames
+ )
)
.Where(constraint => !string.IsNullOrEmpty(constraint));
diff --git a/AutomaticInterface/Tests/Infrastructure.cs b/AutomaticInterface/Tests/Infrastructure.cs
index 2fd51f7..496ba71 100644
--- a/AutomaticInterface/Tests/Infrastructure.cs
+++ b/AutomaticInterface/Tests/Infrastructure.cs
@@ -45,6 +45,7 @@ out var diagnostics
Assert.Empty(errors);
- return outputCompilation.SyntaxTrees.Skip(1).LastOrDefault()?.ToString();
+ // The first syntax tree is the input code, the second two are the two generated attribute classes, and the rest is the generated code.
+ return string.Join(Environment.NewLine + Environment.NewLine, outputCompilation.SyntaxTrees.Skip(3));
}
}
diff --git a/AutomaticInterface/Tests/Methods/Methods.WorksWithGenericMethods.verified.txt b/AutomaticInterface/Tests/Methods/Methods.WorksWithGenericMethods.verified.txt
index 1d810f2..0239653 100644
--- a/AutomaticInterface/Tests/Methods/Methods.WorksWithGenericMethods.verified.txt
+++ b/AutomaticInterface/Tests/Methods/Methods.WorksWithGenericMethods.verified.txt
@@ -12,7 +12,7 @@ namespace AutomaticInterfaceExample
public partial interface IDemoClass
{
///
- string CMethod(string x, string y) where T : class where T1 : struct where T3 : global::AutomaticInterfaceExample.DemoClass where T4 : IDemoClass where T5 : new();
+ string CMethod(string x, string y) where T : class where T1 : struct where T3 : global::AutomaticInterfaceExample.DemoClass where T4 : global::AutomaticInterfaceExample.IDemoClass where T5 : new();
}
}
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedGenericInterfaceReferences.verified.txt b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedGenericInterfaceReferences.verified.txt
new file mode 100644
index 0000000..10967e0
--- /dev/null
+++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedGenericInterfaceReferences.verified.txt
@@ -0,0 +1,41 @@
+//--------------------------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
+//
+//--------------------------------------------------------------------------------------------------
+
+namespace Processor
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ public partial interface IModelProcessor
+ {
+ ///
+ global::Models.IModel Template { get; }
+
+ ///
+ global::Models.IModel> Process(global::Models.IModel model) where T1 : global::Models.IModel>;
+
+ ///
+ event EventHandler> ModelChanged;
+
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
+//
+//--------------------------------------------------------------------------------------------------
+
+namespace Models
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ public partial interface IModel
+ {
+ }
+}
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedInterfaceReferences.verified.txt b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedInterfaceReferences.verified.txt
new file mode 100644
index 0000000..6c26967
--- /dev/null
+++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedInterfaceReferences.verified.txt
@@ -0,0 +1,41 @@
+//--------------------------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
+//
+//--------------------------------------------------------------------------------------------------
+
+namespace Processor
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ public partial interface IModelProcessor
+ {
+ ///
+ global::Models.IModel Template { get; }
+
+ ///
+ global::Models.IModel Process(global::Models.IModel model);
+
+ ///
+ event EventHandler ModelChanged;
+
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
+//
+//--------------------------------------------------------------------------------------------------
+
+namespace Models
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ public partial interface IModel
+ {
+ }
+}
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs
index b67944d..6f7932e 100644
--- a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs
+++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs
@@ -170,4 +170,69 @@ public class DemoClass
await Verify(Infrastructure.GenerateCode(code));
}
+
+ [Fact]
+ public async Task WorksWithGeneratedInterfaceReferences()
+ {
+ const string code = """
+ using AutomaticInterface;
+
+ namespace Processor
+ {
+ using Models;
+
+ [GenerateAutomaticInterface]
+ public class ModelProcessor : IModelProcessor
+ {
+ public IModel Process(IModel model) => null;
+
+ public event EventHandler ModelChanged;
+
+ public IModel Template => null;
+ }
+ }
+
+ namespace Models
+ {
+
+ [GenerateAutomaticInterface]
+ public class Model : IModel;
+ }
+ """;
+
+ await Verify(Infrastructure.GenerateCode(code));
+ }
+
+ [Fact]
+ public async Task WorksWithGeneratedGenericInterfaceReferences()
+ {
+ const string code = """
+ using AutomaticInterface;
+ using System.Collections.Generic;
+
+ namespace Processor
+ {
+ using Models;
+
+ [GenerateAutomaticInterface]
+ public class ModelProcessor : IModelProcessor
+ {
+ public IModel> Process(IModel model) where T1: IModel> => null;
+
+ public event EventHandler> ModelChanged;
+
+ public IModel Template => null;
+ }
+ }
+
+ namespace Models
+ {
+
+ [GenerateAutomaticInterface]
+ public class Model;
+ }
+ """;
+
+ await Verify(Infrastructure.GenerateCode(code));
+ }
}
From d973d0cefaff66c1e9051a736297d85b7933033a Mon Sep 17 00:00:00 2001
From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com>
Date: Wed, 3 Sep 2025 14:46:45 +1000
Subject: [PATCH 2/8] Run csharpier
---
AutomaticInterface/Tests/Infrastructure.cs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/AutomaticInterface/Tests/Infrastructure.cs b/AutomaticInterface/Tests/Infrastructure.cs
index 496ba71..9afe35f 100644
--- a/AutomaticInterface/Tests/Infrastructure.cs
+++ b/AutomaticInterface/Tests/Infrastructure.cs
@@ -46,6 +46,9 @@ out var diagnostics
Assert.Empty(errors);
// The first syntax tree is the input code, the second two are the two generated attribute classes, and the rest is the generated code.
- return string.Join(Environment.NewLine + Environment.NewLine, outputCompilation.SyntaxTrees.Skip(3));
+ return string.Join(
+ Environment.NewLine + Environment.NewLine,
+ outputCompilation.SyntaxTrees.Skip(3)
+ );
}
}
From 5cc46c601fc34b062ebc44b5fa7c821034985123 Mon Sep 17 00:00:00 2001
From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com>
Date: Wed, 3 Sep 2025 20:52:11 +1000
Subject: [PATCH 3/8] Deduplicate "get class syntax and named type symbol"
logic
---
.../DotnetAutomaticInterface/Builder.cs | 36 +++++++++++++------
1 file changed, 26 insertions(+), 10 deletions(-)
diff --git a/AutomaticInterface/DotnetAutomaticInterface/Builder.cs b/AutomaticInterface/DotnetAutomaticInterface/Builder.cs
index a24554b..c2015a4 100644
--- a/AutomaticInterface/DotnetAutomaticInterface/Builder.cs
+++ b/AutomaticInterface/DotnetAutomaticInterface/Builder.cs
@@ -43,14 +43,14 @@ private static string InheritDoc(ISymbol source) =>
public static string? GetInterfaceNameFor(ITypeSymbol typeSymbol)
{
- if (
- typeSymbol.DeclaringSyntaxReferences.First().GetSyntax()
- is not ClassDeclarationSyntax classSyntax
- || typeSymbol is not INamedTypeSymbol
- )
+ var declarationAndNamedTypeSymbol = GetClassDeclarationMetadata(typeSymbol);
+ if (declarationAndNamedTypeSymbol == null)
{
return null;
}
+
+ var (classSyntax, _) = declarationAndNamedTypeSymbol.Value;
+
var symbolDetails = GetSymbolDetails(typeSymbol, classSyntax);
return $"global::{symbolDetails.NamespaceName}.{symbolDetails.InterfaceName}";
@@ -64,15 +64,14 @@ public static string BuildInterfaceFor(
List generatedInterfaceNames
)
{
- if (
- typeSymbol.DeclaringSyntaxReferences.First().GetSyntax()
- is not ClassDeclarationSyntax classSyntax
- || typeSymbol is not INamedTypeSymbol namedTypeSymbol
- )
+ 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,
@@ -101,6 +100,23 @@ is not ClassDeclarationSyntax classSyntax
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
From cf67fd77cf898975134e45adbe2b524aa06ca993 Mon Sep 17 00:00:00 2001
From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com>
Date: Wed, 15 Oct 2025 16:45:40 +1100
Subject: [PATCH 4/8] Simplify replacement of error type names with inferred
names
- Change approach to use `ToDisplayParts`, which removes the need for regex parsing of generated code
- Fix bug in `ReplaceWithInferredInterfaceName` where dots weren't being escaped
- Add tests to ensure partially qualified references will be resolved correctly
---
.../AutomaticInterface/RoslynExtensions.cs | 143 ++++++++++--------
AutomaticInterface/Tests/Infrastructure.cs | 2 +-
...gGeneratedInterfaceReferences.verified.txt | 1 +
...dGeneratedInterfaceReferences.verified.txt | 41 +++++
...encesAndOverlappingNamespaces.verified.txt | 38 +++++
.../Tests/TypeResolutions/TypeResolutions.cs | 63 ++++++++
6 files changed, 225 insertions(+), 63 deletions(-)
create mode 100644 AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithOverlappingGeneratedInterfaceReferences.verified.txt
create mode 100644 AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferences.verified.txt
create mode 100644 AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferencesAndOverlappingNamespaces.verified.txt
diff --git a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs
index 7063a69..8bcabb1 100644
--- a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs
+++ b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs
@@ -86,96 +86,115 @@ public static string ToDisplayString(
this IParameterSymbol symbol,
SymbolDisplayFormat displayFormat,
List generatedInterfaceNames
- )
- {
- var parameterDisplayString = symbol.ToDisplayString(displayFormat);
-
- var parameterTypeDisplayString = symbol.Type.ToDisplayString(
- displayFormat,
- generatedInterfaceNames
- );
-
- // Replace the type part of the parameter definition - we don't try to generate the whole parameter definition
- // as it's quite complex - we leave that to Roslyn.
- return ParameterTypeMatcher.Replace(parameterDisplayString, parameterTypeDisplayString);
- }
+ ) => ToDisplayString((ISymbol)symbol, displayFormat, generatedInterfaceNames);
- ///
- /// Matches the type part of a parameter definition (Type name[ = defaultValue])
- ///
- private static readonly Regex ParameterTypeMatcher =
- new(@"[^\s=]+(?=\s\S+(\s?=\s?\S+)?$)", RegexOptions.Compiled);
+ public static string ToDisplayString(
+ this ITypeSymbol symbol,
+ SymbolDisplayFormat displayFormat,
+ List generatedInterfaceNames
+ ) => ToDisplayString((ISymbol)symbol, displayFormat, generatedInterfaceNames);
///
/// Wraps with custom resolution for generated types
///
- ///
- public static string ToDisplayString(
- this ITypeSymbol symbol,
+ private static string ToDisplayString(
+ this ISymbol symbol,
SymbolDisplayFormat displayFormat,
List generatedInterfaceNames
)
{
- var builder = new StringBuilder();
+ var displayStringBuilder = new StringBuilder();
- AppendTypeSymbolDisplayString(symbol, displayFormat, generatedInterfaceNames, builder);
+ var displayParts = GetDisplayParts(symbol, displayFormat);
+
+ foreach (var part in displayParts)
+ {
+ if (part.Kind == SymbolDisplayPartKind.ErrorTypeName)
+ {
+ var unrecognisedName = part.ToString();
+
+ var inferredName = ReplaceWithInferredInterfaceName(
+ unrecognisedName,
+ generatedInterfaceNames
+ );
+
+ displayStringBuilder.Append(inferredName);
+ }
+ else
+ {
+ displayStringBuilder.Append(part);
+ }
+ }
- return builder.ToString();
+ return displayStringBuilder.ToString();
}
- private static void AppendTypeSymbolDisplayString(
- ITypeSymbol typeSymbol,
- SymbolDisplayFormat displayFormat,
- List generatedInterfaceNames,
- StringBuilder builder
+ ///
+ /// The same as but with adjacent SymbolDisplayParts merged into qualified type references, e.g. [Parent, ., Child] => Parent.Child
+ ///
+ private static IEnumerable GetDisplayParts(
+ ISymbol symbol,
+ SymbolDisplayFormat displayFormat
)
{
- if (typeSymbol is not IErrorTypeSymbol errorTypeSymbol)
+ var cache = new List();
+
+ foreach (var part in symbol.ToDisplayParts(displayFormat))
{
- // This symbol contains no unresolved types. Fall back to the default generation provided by Roslyn
- builder.Append(typeSymbol.ToDisplayString(displayFormat));
- return;
- }
+ if (cache.Count == 0)
+ {
+ cache.Add(part);
+ continue;
+ }
- var symbolName =
- InferGeneratedInterfaceName(errorTypeSymbol, generatedInterfaceNames)
- ?? errorTypeSymbol.Name;
+ var previousPart = cache.Last();
- builder.Append(symbolName);
+ if (
+ IsPartQualificationPunctuation(previousPart)
+ ^ IsPartQualificationPunctuation(part)
+ )
+ {
+ cache.Add(part);
+ }
+ else
+ {
+ yield return CombineQualifiedTypeParts(cache);
+ cache.Clear();
+ cache.Add(part);
+ }
+ }
- if (errorTypeSymbol.IsGenericType)
+ if (cache.Count > 0)
{
- builder.Append('<');
+ yield return CombineQualifiedTypeParts(cache);
+ }
- bool isFirstTypeArgument = true;
- foreach (var typeArgument in errorTypeSymbol.TypeArguments)
- {
- if (!isFirstTypeArgument)
- {
- builder.Append(", ");
- }
-
- AppendTypeSymbolDisplayString(
- typeArgument,
- displayFormat,
- generatedInterfaceNames,
- builder
+ static SymbolDisplayPart CombineQualifiedTypeParts(
+ ICollection qualifiedTypeParts
+ )
+ {
+ var qualifiedType = qualifiedTypeParts.Last();
+
+ return qualifiedTypeParts.Count == 1
+ ? qualifiedType
+ : new SymbolDisplayPart(
+ qualifiedType.Kind,
+ qualifiedType.Symbol,
+ string.Join("", qualifiedTypeParts)
);
-
- isFirstTypeArgument = false;
- }
-
- builder.Append('>');
}
+
+ static bool IsPartQualificationPunctuation(SymbolDisplayPart part) =>
+ part.ToString() is "." or "::";
}
- private static string? InferGeneratedInterfaceName(
- IErrorTypeSymbol unrecognisedSymbol,
+ private static string ReplaceWithInferredInterfaceName(
+ string unrecognisedName,
List generatedInterfaceNames
)
{
var matches = generatedInterfaceNames
- .Where(name => Regex.IsMatch(name, $"[.:]{unrecognisedSymbol.Name}$"))
+ .Where(name => Regex.IsMatch(name, $"[.:]{Regex.Escape(unrecognisedName)}$"))
.ToList();
if (matches.Count != 1)
@@ -183,7 +202,7 @@ List generatedInterfaceNames
// Either there's no match or an ambiguous match - we can't safely infer the interface name.
// This is very much a "best effort" approach - if there are two interfaces with the same name,
// there's no obvious way to work out which one the symbol is referring to.
- return null;
+ return unrecognisedName;
}
return matches[0];
diff --git a/AutomaticInterface/Tests/Infrastructure.cs b/AutomaticInterface/Tests/Infrastructure.cs
index 9afe35f..3d697a6 100644
--- a/AutomaticInterface/Tests/Infrastructure.cs
+++ b/AutomaticInterface/Tests/Infrastructure.cs
@@ -26,7 +26,7 @@ public static string GenerateCode(string code)
var sourceDiagnostics = compilation.GetDiagnostics();
var sourceErrors = sourceDiagnostics
.Where(d => d.Severity == DiagnosticSeverity.Error)
- .Where(x => x.Id != "CS0246") // missing references are ok
+ .Where(x => x.Id != "CS0246" && x.Id != "CS0234") // missing references are ok
.ToList();
Assert.Empty(sourceErrors);
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithOverlappingGeneratedInterfaceReferences.verified.txt b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithOverlappingGeneratedInterfaceReferences.verified.txt
new file mode 100644
index 0000000..5f28270
--- /dev/null
+++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithOverlappingGeneratedInterfaceReferences.verified.txt
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferences.verified.txt b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferences.verified.txt
new file mode 100644
index 0000000..47ff738
--- /dev/null
+++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferences.verified.txt
@@ -0,0 +1,41 @@
+//--------------------------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
+//
+//--------------------------------------------------------------------------------------------------
+
+namespace Processor
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ public partial interface IModelProcessor
+ {
+ ///
+ global::ModelsRoot.Models.IModel Template { get; }
+
+ ///
+ global::ModelsRoot.Models.IModel Process(global::ModelsRoot.Models.IModel model);
+
+ ///
+ event EventHandler ModelChanged;
+
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
+//
+//--------------------------------------------------------------------------------------------------
+
+namespace ModelsRoot.Models
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ public partial interface IModel
+ {
+ }
+}
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferencesAndOverlappingNamespaces.verified.txt b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferencesAndOverlappingNamespaces.verified.txt
new file mode 100644
index 0000000..de8da35
--- /dev/null
+++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferencesAndOverlappingNamespaces.verified.txt
@@ -0,0 +1,38 @@
+//--------------------------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
+//
+//--------------------------------------------------------------------------------------------------
+
+namespace Root.Processor
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ public partial interface IModelProcessor
+ {
+ ///
+ global::Root.ModelsRoot.Models.IModel ProcessFullyQualified(global::Root.ModelsRoot.Models.IModel model);
+
+ ///
+ global::Root.ModelsRoot.Models.IModel ProcessRelativeQualified(global::Root.ModelsRoot.Models.IModel model);
+
+ }
+}
+
+
+//--------------------------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
+//
+//--------------------------------------------------------------------------------------------------
+
+namespace Root.ModelsRoot.Models
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ public partial interface IModel
+ {
+ }
+}
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs
index 6f7932e..e178388 100644
--- a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs
+++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs
@@ -235,4 +235,67 @@ public class Model;
await Verify(Infrastructure.GenerateCode(code));
}
+
+ [Fact]
+ public async Task WorksWithQualifiedGeneratedInterfaceReferences()
+ {
+ const string code = """
+ using AutomaticInterface;
+
+ namespace Processor
+ {
+ using ModelsRoot;
+
+ [GenerateAutomaticInterface]
+ public class ModelProcessor : IModelProcessor
+ {
+ public Models.IModel Process(Models.IModel model) => null;
+
+ public event EventHandler ModelChanged;
+
+ public Models.IModel Template => null;
+ }
+ }
+
+ namespace ModelsRoot.Models
+ {
+
+ [GenerateAutomaticInterface]
+ public class Model : IModel;
+ }
+ """;
+
+ await Verify(Infrastructure.GenerateCode(code));
+ }
+
+ [Fact]
+ public async Task WorksWithQualifiedGeneratedInterfaceReferencesAndOverlappingNamespaces()
+ {
+ const string code = """
+ using AutomaticInterface;
+
+ namespace Root
+ {
+ namespace Processor
+ {
+ [GenerateAutomaticInterface]
+ public class ModelProcessor : IModelProcessor
+ {
+ public Root.ModelsRoot.Models.IModel ProcessFullyQualified(Root.ModelsRoot.Models.IModel model) => null;
+
+ public ModelsRoot.Models.IModel ProcessRelativeQualified(ModelsRoot.Models.IModel model) => null;
+ }
+ }
+
+ namespace ModelsRoot.Models
+ {
+
+ [GenerateAutomaticInterface]
+ public class Model : IModel;
+ }
+ }
+ """;
+
+ await Verify(Infrastructure.GenerateCode(code));
+ }
}
From 136467c925bfa3d2f7c7f0c04d6b0485474058c5 Mon Sep 17 00:00:00 2001
From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com>
Date: Wed, 19 Nov 2025 10:11:45 +1100
Subject: [PATCH 5/8] Move `Builder.GetParameterDisplayString` into the
`RoslynExtensions` class; refactor
This centralises the rendering logic plus simplifies the implementation to use `ITypeSymbol.WithNullableAnnotation` and hands the rendering off to the default `ToDisplayString` implementation
---
.../AutomaticInterface/RoslynExtensions.cs | 39 +++++++++++++++++--
.../DotnetAutomaticInterface/Builder.cs | 31 ++++-----------
...thMixedOptionalNullParameters.verified.txt | 2 +-
3 files changed, 43 insertions(+), 29 deletions(-)
diff --git a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs
index 8bcabb1..6776397 100644
--- a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs
+++ b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
@@ -85,8 +86,36 @@ List generatedInterfaceNames
public static string ToDisplayString(
this IParameterSymbol symbol,
SymbolDisplayFormat displayFormat,
+ bool nullableContextEnabled,
List generatedInterfaceNames
- ) => ToDisplayString((ISymbol)symbol, displayFormat, generatedInterfaceNames);
+ )
+ {
+ string? RenderTypeSymbolWithNullableAnnotation(SymbolDisplayPart part) =>
+ part.Symbol is ITypeSymbol typeSymbol
+ ? typeSymbol
+ .WithNullableAnnotation(NullableAnnotation.Annotated)
+ .ToDisplayString(displayFormat)
+ : null;
+
+ // Special case for reference parameters with default value null (e.g. string x = null) - the nullable
+ // context isn't applied automatically, so it must be forced explicitly
+ var forceNullableAnnotation =
+ nullableContextEnabled
+ && symbol
+ is {
+ Type.IsReferenceType: true,
+ HasExplicitDefaultValue: true,
+ ExplicitDefaultValue: null
+ }
+ && symbol.NullableAnnotation != NullableAnnotation.Annotated;
+
+ return ToDisplayString(
+ symbol,
+ displayFormat,
+ generatedInterfaceNames,
+ forceNullableAnnotation ? RenderTypeSymbolWithNullableAnnotation : null
+ );
+ }
public static string ToDisplayString(
this ITypeSymbol symbol,
@@ -100,7 +129,8 @@ List generatedInterfaceNames
private static string ToDisplayString(
this ISymbol symbol,
SymbolDisplayFormat displayFormat,
- List generatedInterfaceNames
+ List generatedInterfaceNames,
+ Func? customRenderDisplayPart = null
)
{
var displayStringBuilder = new StringBuilder();
@@ -122,7 +152,8 @@ List generatedInterfaceNames
}
else
{
- displayStringBuilder.Append(part);
+ var customRender = customRenderDisplayPart?.Invoke(part);
+ displayStringBuilder.Append(customRender ?? part.ToString());
}
}
diff --git a/AutomaticInterface/DotnetAutomaticInterface/Builder.cs b/AutomaticInterface/DotnetAutomaticInterface/Builder.cs
index c2015a4..68793b3 100644
--- a/AutomaticInterface/DotnetAutomaticInterface/Builder.cs
+++ b/AutomaticInterface/DotnetAutomaticInterface/Builder.cs
@@ -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;
@@ -163,7 +162,13 @@ List generatedInterfaceNames
var paramResult = new HashSet();
method
- .Parameters.Select(p => GetParameterDisplayString(p, codeGenerator.HasNullable))
+ .Parameters.Select(p =>
+ p.ToDisplayString(
+ FullyQualifiedDisplayFormat,
+ codeGenerator.HasNullable,
+ generatedInterfaceNames
+ )
+ )
.ToList()
.ForEach(x => paramResult.Add(x));
@@ -234,28 +239,6 @@ private static bool IsNullable(ITypeSymbol typeSymbol)
return false;
}
- private static string GetParameterDisplayString(
- IParameterSymbol param,
- bool nullableContextEnabled
- )
- {
- // 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 members,
InterfaceBuilder codeGenerator,
diff --git a/AutomaticInterface/Tests/Methods/Methods.WorksWithMixedOptionalNullParameters.verified.txt b/AutomaticInterface/Tests/Methods/Methods.WorksWithMixedOptionalNullParameters.verified.txt
index d5e2d0f..4fda202 100644
--- a/AutomaticInterface/Tests/Methods/Methods.WorksWithMixedOptionalNullParameters.verified.txt
+++ b/AutomaticInterface/Tests/Methods/Methods.WorksWithMixedOptionalNullParameters.verified.txt
@@ -13,7 +13,7 @@ namespace AutomaticInterfaceExample
public partial interface IDemoClass
{
///
- bool TryStartTransaction(int? param, int param2 = 0, string? data = null, Func? func = null);
+ bool TryStartTransaction(int? param, int param2 = 0, string? data = null, Func func = null);
}
}
From 44b679e41832bf560b5c400ced90ee6fa827a8cf Mon Sep 17 00:00:00 2001
From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com>
Date: Wed, 19 Nov 2025 10:18:11 +1100
Subject: [PATCH 6/8] Increment package version, update changelog
---
README.md | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index aa9adb0..4ab7d98 100644
--- a/README.md
+++ b/README.md
@@ -189,6 +189,11 @@ Note that we use [Verify](https://github.com/VerifyTests/Verify) for testing. It
## Changelog
+### 6.0.2
+
+- Added feature to allow generated interfaces to reference other generated interfaces
+
+
### 6.0.0
Forked from [AutomaticInterface](https://github.com/codecentric/net_automatic_interface) because I no longer work there and it is uncertain if somebody takes it over.
@@ -199,7 +204,7 @@ Forked from [AutomaticInterface](https://github.com/codecentric/net_automatic_in
### 5.2.6
-- Fix wrong documenation on interface for this library
+- Fix wrong documentation on interface for this library
- Fix handling of parameters with default null. Thanks paramamue!
### 5.2.5
From c97bc87b02cef7167ac97408e04fbdde7bbbf96d Mon Sep 17 00:00:00 2001
From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com>
Date: Fri, 24 Apr 2026 08:34:58 +1000
Subject: [PATCH 7/8] Update to match updated project folder structure and
namespace
---
.../AutomaticInterface/RoslynExtensions.cs | 242 ----------------
.../RoslynExtensions.cs | 261 ++++++++++++++----
AutomaticInterface/TestNuget/Test.cs | 2 +-
...tedGenericInterfaceReferences.verified.txt | 4 +-
...hGeneratedInterfaceReferences.verified.txt | 4 +-
...gGeneratedInterfaceReferences.verified.txt | 1 -
...dGeneratedInterfaceReferences.verified.txt | 4 +-
...encesAndOverlappingNamespaces.verified.txt | 4 +-
.../Tests/TypeResolutions/TypeResolutions.cs | 8 +-
README.md | 2 +-
10 files changed, 225 insertions(+), 307 deletions(-)
delete mode 100644 AutomaticInterface/AutomaticInterface/RoslynExtensions.cs
delete mode 100644 AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithOverlappingGeneratedInterfaceReferences.verified.txt
diff --git a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs
deleted file mode 100644
index 6776397..0000000
--- a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs
+++ /dev/null
@@ -1,242 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-
-namespace AutomaticInterface
-{
- ///
- /// Source: https://github.com/dominikjeske/Samples/blob/main/SourceGenerators/HomeCenter.SourceGenerators/Extensions/RoslynExtensions.cs
- ///
- public static class RoslynExtensions
- {
- private static IEnumerable GetBaseTypesAndThis(this ITypeSymbol type)
- {
- var current = type;
- while (current != null)
- {
- yield return current;
- current = current.BaseType;
- }
- }
-
- public static IEnumerable GetAllMembers(this ITypeSymbol type)
- {
- return type.GetBaseTypesAndThis().SelectMany(n => n.GetMembers());
- }
-
- public static string GetClassName(this ClassDeclarationSyntax proxy)
- {
- return proxy.Identifier.Text;
- }
-
- ///
- /// Thanks to https://www.codeproject.com/Articles/871704/Roslyn-Code-Analysis-in-Easy-Samples-Part-2
- ///
- public static string GetWhereStatement(
- this ITypeParameterSymbol typeParameterSymbol,
- SymbolDisplayFormat typeDisplayFormat,
- List generatedInterfaceNames
- )
- {
- var result = $"where {typeParameterSymbol.Name} : ";
-
- var constraints = new List();
-
- if (typeParameterSymbol.HasReferenceTypeConstraint)
- {
- constraints.Add("class");
- }
-
- if (typeParameterSymbol.HasValueTypeConstraint)
- {
- constraints.Add("struct");
- }
-
- if (typeParameterSymbol.HasNotNullConstraint)
- {
- constraints.Add("notnull");
- }
-
- constraints.AddRange(
- typeParameterSymbol.ConstraintTypes.Select(t =>
- t.ToDisplayString(typeDisplayFormat, generatedInterfaceNames)
- )
- );
-
- // The new() constraint must be last
- if (typeParameterSymbol.HasConstructorConstraint)
- {
- constraints.Add("new()");
- }
-
- if (constraints.Count == 0)
- {
- return "";
- }
-
- result += string.Join(", ", constraints);
-
- return result;
- }
-
- public static string ToDisplayString(
- this IParameterSymbol symbol,
- SymbolDisplayFormat displayFormat,
- bool nullableContextEnabled,
- List generatedInterfaceNames
- )
- {
- string? RenderTypeSymbolWithNullableAnnotation(SymbolDisplayPart part) =>
- part.Symbol is ITypeSymbol typeSymbol
- ? typeSymbol
- .WithNullableAnnotation(NullableAnnotation.Annotated)
- .ToDisplayString(displayFormat)
- : null;
-
- // Special case for reference parameters with default value null (e.g. string x = null) - the nullable
- // context isn't applied automatically, so it must be forced explicitly
- var forceNullableAnnotation =
- nullableContextEnabled
- && symbol
- is {
- Type.IsReferenceType: true,
- HasExplicitDefaultValue: true,
- ExplicitDefaultValue: null
- }
- && symbol.NullableAnnotation != NullableAnnotation.Annotated;
-
- return ToDisplayString(
- symbol,
- displayFormat,
- generatedInterfaceNames,
- forceNullableAnnotation ? RenderTypeSymbolWithNullableAnnotation : null
- );
- }
-
- public static string ToDisplayString(
- this ITypeSymbol symbol,
- SymbolDisplayFormat displayFormat,
- List generatedInterfaceNames
- ) => ToDisplayString((ISymbol)symbol, displayFormat, generatedInterfaceNames);
-
- ///
- /// Wraps with custom resolution for generated types
- ///
- private static string ToDisplayString(
- this ISymbol symbol,
- SymbolDisplayFormat displayFormat,
- List generatedInterfaceNames,
- Func? customRenderDisplayPart = null
- )
- {
- var displayStringBuilder = new StringBuilder();
-
- var displayParts = GetDisplayParts(symbol, displayFormat);
-
- foreach (var part in displayParts)
- {
- if (part.Kind == SymbolDisplayPartKind.ErrorTypeName)
- {
- var unrecognisedName = part.ToString();
-
- var inferredName = ReplaceWithInferredInterfaceName(
- unrecognisedName,
- generatedInterfaceNames
- );
-
- displayStringBuilder.Append(inferredName);
- }
- else
- {
- var customRender = customRenderDisplayPart?.Invoke(part);
- displayStringBuilder.Append(customRender ?? part.ToString());
- }
- }
-
- return displayStringBuilder.ToString();
- }
-
- ///
- /// The same as but with adjacent SymbolDisplayParts merged into qualified type references, e.g. [Parent, ., Child] => Parent.Child
- ///
- private static IEnumerable GetDisplayParts(
- ISymbol symbol,
- SymbolDisplayFormat displayFormat
- )
- {
- var cache = new List();
-
- foreach (var part in symbol.ToDisplayParts(displayFormat))
- {
- if (cache.Count == 0)
- {
- cache.Add(part);
- continue;
- }
-
- var previousPart = cache.Last();
-
- if (
- IsPartQualificationPunctuation(previousPart)
- ^ IsPartQualificationPunctuation(part)
- )
- {
- cache.Add(part);
- }
- else
- {
- yield return CombineQualifiedTypeParts(cache);
- cache.Clear();
- cache.Add(part);
- }
- }
-
- if (cache.Count > 0)
- {
- yield return CombineQualifiedTypeParts(cache);
- }
-
- static SymbolDisplayPart CombineQualifiedTypeParts(
- ICollection qualifiedTypeParts
- )
- {
- var qualifiedType = qualifiedTypeParts.Last();
-
- return qualifiedTypeParts.Count == 1
- ? qualifiedType
- : new SymbolDisplayPart(
- qualifiedType.Kind,
- qualifiedType.Symbol,
- string.Join("", qualifiedTypeParts)
- );
- }
-
- static bool IsPartQualificationPunctuation(SymbolDisplayPart part) =>
- part.ToString() is "." or "::";
- }
-
- private static string ReplaceWithInferredInterfaceName(
- string unrecognisedName,
- List generatedInterfaceNames
- )
- {
- var matches = generatedInterfaceNames
- .Where(name => Regex.IsMatch(name, $"[.:]{Regex.Escape(unrecognisedName)}$"))
- .ToList();
-
- if (matches.Count != 1)
- {
- // Either there's no match or an ambiguous match - we can't safely infer the interface name.
- // This is very much a "best effort" approach - if there are two interfaces with the same name,
- // there's no obvious way to work out which one the symbol is referring to.
- return unrecognisedName;
- }
-
- return matches[0];
- }
- }
-}
diff --git a/AutomaticInterface/DotnetAutomaticInterface/RoslynExtensions.cs b/AutomaticInterface/DotnetAutomaticInterface/RoslynExtensions.cs
index ff7ca97..a6e57a0 100644
--- a/AutomaticInterface/DotnetAutomaticInterface/RoslynExtensions.cs
+++ b/AutomaticInterface/DotnetAutomaticInterface/RoslynExtensions.cs
@@ -1,81 +1,242 @@
using System;
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
-namespace DotnetAutomaticInterface;
-
-///
-/// Source: https://github.com/dominikjeske/Samples/blob/main/SourceGenerators/HomeCenter.SourceGenerators/Extensions/RoslynExtensions.cs
-///
-public static class RoslynExtensions
+namespace DotnetAutomaticInterface
{
- private static IEnumerable GetBaseTypesAndThis(this ITypeSymbol type)
- {
- var current = type;
- while (current != null)
- {
- yield return current;
- current = current.BaseType;
- }
- }
-
- public static IEnumerable GetAllMembers(this ITypeSymbol type)
- {
- return type.GetBaseTypesAndThis().SelectMany(n => n.GetMembers());
- }
-
- public static string GetClassName(this ClassDeclarationSyntax proxy)
- {
- return proxy.Identifier.Text;
- }
-
///
- /// Thanks to https://www.codeproject.com/Articles/871704/Roslyn-Code-Analysis-in-Easy-Samples-Part-2
+ /// Source: https://github.com/dominikjeske/Samples/blob/main/SourceGenerators/HomeCenter.SourceGenerators/Extensions/RoslynExtensions.cs
///
- public static string GetWhereStatement(
- this ITypeParameterSymbol typeParameterSymbol,
- SymbolDisplayFormat typeDisplayFormat
- )
+ public static class RoslynExtensions
{
- var result = $"where {typeParameterSymbol.Name} : ";
+ private static IEnumerable GetBaseTypesAndThis(this ITypeSymbol type)
+ {
+ var current = type;
+ while (current != null)
+ {
+ yield return current;
+ current = current.BaseType;
+ }
+ }
- var constraints = new List();
+ public static IEnumerable GetAllMembers(this ITypeSymbol type)
+ {
+ return type.GetBaseTypesAndThis().SelectMany(n => n.GetMembers());
+ }
- if (typeParameterSymbol.HasReferenceTypeConstraint)
+ public static string GetClassName(this ClassDeclarationSyntax proxy)
{
- constraints.Add("class");
+ return proxy.Identifier.Text;
}
- if (typeParameterSymbol.HasValueTypeConstraint)
+ ///
+ /// Thanks to https://www.codeproject.com/Articles/871704/Roslyn-Code-Analysis-in-Easy-Samples-Part-2
+ ///
+ public static string GetWhereStatement(
+ this ITypeParameterSymbol typeParameterSymbol,
+ SymbolDisplayFormat typeDisplayFormat,
+ List generatedInterfaceNames
+ )
{
- constraints.Add("struct");
+ var result = $"where {typeParameterSymbol.Name} : ";
+
+ var constraints = new List();
+
+ if (typeParameterSymbol.HasReferenceTypeConstraint)
+ {
+ constraints.Add("class");
+ }
+
+ if (typeParameterSymbol.HasValueTypeConstraint)
+ {
+ constraints.Add("struct");
+ }
+
+ if (typeParameterSymbol.HasNotNullConstraint)
+ {
+ constraints.Add("notnull");
+ }
+
+ constraints.AddRange(
+ typeParameterSymbol.ConstraintTypes.Select(t =>
+ t.ToDisplayString(typeDisplayFormat, generatedInterfaceNames)
+ )
+ );
+
+ // The new() constraint must be last
+ if (typeParameterSymbol.HasConstructorConstraint)
+ {
+ constraints.Add("new()");
+ }
+
+ if (constraints.Count == 0)
+ {
+ return "";
+ }
+
+ result += string.Join(", ", constraints);
+
+ return result;
}
- if (typeParameterSymbol.HasNotNullConstraint)
+ public static string ToDisplayString(
+ this IParameterSymbol symbol,
+ SymbolDisplayFormat displayFormat,
+ bool nullableContextEnabled,
+ List generatedInterfaceNames
+ )
{
- constraints.Add("notnull");
+ string? RenderTypeSymbolWithNullableAnnotation(SymbolDisplayPart part) =>
+ part.Symbol is ITypeSymbol typeSymbol
+ ? typeSymbol
+ .WithNullableAnnotation(NullableAnnotation.Annotated)
+ .ToDisplayString(displayFormat)
+ : null;
+
+ // Special case for reference parameters with default value null (e.g. string x = null) - the nullable
+ // context isn't applied automatically, so it must be forced explicitly
+ var forceNullableAnnotation =
+ nullableContextEnabled
+ && symbol
+ is {
+ Type.IsReferenceType: true,
+ HasExplicitDefaultValue: true,
+ ExplicitDefaultValue: null
+ }
+ && symbol.NullableAnnotation != NullableAnnotation.Annotated;
+
+ return ToDisplayString(
+ symbol,
+ displayFormat,
+ generatedInterfaceNames,
+ forceNullableAnnotation ? RenderTypeSymbolWithNullableAnnotation : null
+ );
}
- constraints.AddRange(
- typeParameterSymbol.ConstraintTypes.Select(t => t.ToDisplayString(typeDisplayFormat))
- );
+ public static string ToDisplayString(
+ this ITypeSymbol symbol,
+ SymbolDisplayFormat displayFormat,
+ List generatedInterfaceNames
+ ) => ToDisplayString((ISymbol)symbol, displayFormat, generatedInterfaceNames);
- // The new() constraint must be last
- if (typeParameterSymbol.HasConstructorConstraint)
+ ///
+ /// Wraps with custom resolution for generated types
+ ///
+ private static string ToDisplayString(
+ this ISymbol symbol,
+ SymbolDisplayFormat displayFormat,
+ List generatedInterfaceNames,
+ Func? customRenderDisplayPart = null
+ )
{
- constraints.Add("new()");
+ var displayStringBuilder = new StringBuilder();
+
+ var displayParts = GetDisplayParts(symbol, displayFormat);
+
+ foreach (var part in displayParts)
+ {
+ if (part.Kind == SymbolDisplayPartKind.ErrorTypeName)
+ {
+ var unrecognisedName = part.ToString();
+
+ var inferredName = ReplaceWithInferredInterfaceName(
+ unrecognisedName,
+ generatedInterfaceNames
+ );
+
+ displayStringBuilder.Append(inferredName);
+ }
+ else
+ {
+ var customRender = customRenderDisplayPart?.Invoke(part);
+ displayStringBuilder.Append(customRender ?? part.ToString());
+ }
+ }
+
+ return displayStringBuilder.ToString();
}
- if (constraints.Count == 0)
+ ///
+ /// The same as but with adjacent SymbolDisplayParts merged into qualified type references, e.g. [Parent, ., Child] => Parent.Child
+ ///
+ private static IEnumerable GetDisplayParts(
+ ISymbol symbol,
+ SymbolDisplayFormat displayFormat
+ )
{
- return "";
+ var cache = new List();
+
+ foreach (var part in symbol.ToDisplayParts(displayFormat))
+ {
+ if (cache.Count == 0)
+ {
+ cache.Add(part);
+ continue;
+ }
+
+ var previousPart = cache.Last();
+
+ if (
+ IsPartQualificationPunctuation(previousPart)
+ ^ IsPartQualificationPunctuation(part)
+ )
+ {
+ cache.Add(part);
+ }
+ else
+ {
+ yield return CombineQualifiedTypeParts(cache);
+ cache.Clear();
+ cache.Add(part);
+ }
+ }
+
+ if (cache.Count > 0)
+ {
+ yield return CombineQualifiedTypeParts(cache);
+ }
+
+ static SymbolDisplayPart CombineQualifiedTypeParts(
+ ICollection qualifiedTypeParts
+ )
+ {
+ var qualifiedType = qualifiedTypeParts.Last();
+
+ return qualifiedTypeParts.Count == 1
+ ? qualifiedType
+ : new SymbolDisplayPart(
+ qualifiedType.Kind,
+ qualifiedType.Symbol,
+ string.Join("", qualifiedTypeParts)
+ );
+ }
+
+ static bool IsPartQualificationPunctuation(SymbolDisplayPart part) =>
+ part.ToString() is "." or "::";
}
- result += string.Join(", ", constraints);
+ private static string ReplaceWithInferredInterfaceName(
+ string unrecognisedName,
+ List generatedInterfaceNames
+ )
+ {
+ var matches = generatedInterfaceNames
+ .Where(name => Regex.IsMatch(name, $"[.:]{Regex.Escape(unrecognisedName)}$"))
+ .ToList();
- return result;
+ if (matches.Count != 1)
+ {
+ // Either there's no match or an ambiguous match - we can't safely infer the interface name.
+ // This is very much a "best effort" approach - if there are two interfaces with the same name,
+ // there's no obvious way to work out which one the symbol is referring to.
+ return unrecognisedName;
+ }
+
+ return matches[0];
+ }
}
}
diff --git a/AutomaticInterface/TestNuget/Test.cs b/AutomaticInterface/TestNuget/Test.cs
index 7f758a4..ef216e7 100644
--- a/AutomaticInterface/TestNuget/Test.cs
+++ b/AutomaticInterface/TestNuget/Test.cs
@@ -1,4 +1,4 @@
-using AutomaticInterface;
+using DotnetAutomaticInterface;
namespace TestNuget;
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedGenericInterfaceReferences.verified.txt b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedGenericInterfaceReferences.verified.txt
index 10967e0..68d59c1 100644
--- a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedGenericInterfaceReferences.verified.txt
+++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedGenericInterfaceReferences.verified.txt
@@ -8,7 +8,7 @@
namespace Processor
{
- [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ [global::System.CodeDom.Compiler.GeneratedCode("DotnetAutomaticInterface", "")]
public partial interface IModelProcessor
{
///
@@ -34,7 +34,7 @@ namespace Processor
namespace Models
{
- [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ [global::System.CodeDom.Compiler.GeneratedCode("DotnetAutomaticInterface", "")]
public partial interface IModel
{
}
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedInterfaceReferences.verified.txt b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedInterfaceReferences.verified.txt
index 6c26967..d1ea24d 100644
--- a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedInterfaceReferences.verified.txt
+++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedInterfaceReferences.verified.txt
@@ -8,7 +8,7 @@
namespace Processor
{
- [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ [global::System.CodeDom.Compiler.GeneratedCode("DotnetAutomaticInterface", "")]
public partial interface IModelProcessor
{
///
@@ -34,7 +34,7 @@ namespace Processor
namespace Models
{
- [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ [global::System.CodeDom.Compiler.GeneratedCode("DotnetAutomaticInterface", "")]
public partial interface IModel
{
}
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithOverlappingGeneratedInterfaceReferences.verified.txt b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithOverlappingGeneratedInterfaceReferences.verified.txt
deleted file mode 100644
index 5f28270..0000000
--- a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithOverlappingGeneratedInterfaceReferences.verified.txt
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferences.verified.txt b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferences.verified.txt
index 47ff738..0cab8af 100644
--- a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferences.verified.txt
+++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferences.verified.txt
@@ -8,7 +8,7 @@
namespace Processor
{
- [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ [global::System.CodeDom.Compiler.GeneratedCode("DotnetAutomaticInterface", "")]
public partial interface IModelProcessor
{
///
@@ -34,7 +34,7 @@ namespace Processor
namespace ModelsRoot.Models
{
- [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ [global::System.CodeDom.Compiler.GeneratedCode("DotnetAutomaticInterface", "")]
public partial interface IModel
{
}
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferencesAndOverlappingNamespaces.verified.txt b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferencesAndOverlappingNamespaces.verified.txt
index de8da35..de68055 100644
--- a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferencesAndOverlappingNamespaces.verified.txt
+++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferencesAndOverlappingNamespaces.verified.txt
@@ -8,7 +8,7 @@
namespace Root.Processor
{
- [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ [global::System.CodeDom.Compiler.GeneratedCode("DotnetAutomaticInterface", "")]
public partial interface IModelProcessor
{
///
@@ -31,7 +31,7 @@ namespace Root.Processor
namespace Root.ModelsRoot.Models
{
- [global::System.CodeDom.Compiler.GeneratedCode("AutomaticInterface", "")]
+ [global::System.CodeDom.Compiler.GeneratedCode("DotnetAutomaticInterface", "")]
public partial interface IModel
{
}
diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs
index e178388..fa5bff9 100644
--- a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs
+++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs
@@ -175,7 +175,7 @@ public class DemoClass
public async Task WorksWithGeneratedInterfaceReferences()
{
const string code = """
- using AutomaticInterface;
+ using DotnetAutomaticInterface;
namespace Processor
{
@@ -207,7 +207,7 @@ public class Model : IModel;
public async Task WorksWithGeneratedGenericInterfaceReferences()
{
const string code = """
- using AutomaticInterface;
+ using DotnetAutomaticInterface;
using System.Collections.Generic;
namespace Processor
@@ -240,7 +240,7 @@ public class Model;
public async Task WorksWithQualifiedGeneratedInterfaceReferences()
{
const string code = """
- using AutomaticInterface;
+ using DotnetAutomaticInterface;
namespace Processor
{
@@ -272,7 +272,7 @@ public class Model : IModel;
public async Task WorksWithQualifiedGeneratedInterfaceReferencesAndOverlappingNamespaces()
{
const string code = """
- using AutomaticInterface;
+ using DotnetAutomaticInterface;
namespace Root
{
diff --git a/README.md b/README.md
index 4ab7d98..a0fe33f 100644
--- a/README.md
+++ b/README.md
@@ -138,7 +138,7 @@ namespace AutomaticInterfaceExample
## How to use it?
1. Install the nuget: `dotnet add package AutomaticInterface`.
-2. Add `using AutomaticInterface;` or (Pro-tip) add `global using AutomaticInterface;` to your GlobalUsings.
+2. Add `using DotnetAutomaticInterface;` or (Pro-tip) add `global using DotnetAutomaticInterface;` to your GlobalUsings.
3. Tag your class with the `[GenerateAutomaticInterface]` attribute.
4. The Interface should now be available.
From 3248feb1773ff96305e0862a53fa514c81f73ed3 Mon Sep 17 00:00:00 2001
From: Simon McKenzie <17579442+simonmckenzie@users.noreply.github.com>
Date: Wed, 13 May 2026 14:09:07 +1000
Subject: [PATCH 8/8] Revert RoslynExtensions to the version in the main
branch; move enhancements/extensions into a new project-specific class,
`RoslynExtensionsAutomaticInterface`
---
.../RoslynExtensions.cs | 264 ++++--------------
.../RoslynExtensionsAutomaticInterface.cs | 218 +++++++++++++++
2 files changed, 271 insertions(+), 211 deletions(-)
create mode 100644 AutomaticInterface/DotnetAutomaticInterface/RoslynExtensionsAutomaticInterface.cs
diff --git a/AutomaticInterface/DotnetAutomaticInterface/RoslynExtensions.cs b/AutomaticInterface/DotnetAutomaticInterface/RoslynExtensions.cs
index a6e57a0..34588a8 100644
--- a/AutomaticInterface/DotnetAutomaticInterface/RoslynExtensions.cs
+++ b/AutomaticInterface/DotnetAutomaticInterface/RoslynExtensions.cs
@@ -1,242 +1,84 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
-namespace DotnetAutomaticInterface
+namespace DotnetAutomaticInterface;
+
+///
+/// Source: https://github.com/dominikjeske/Samples/blob/main/SourceGenerators/HomeCenter.SourceGenerators/Extensions/RoslynExtensions.cs
+///
+///
+/// Enhancements or additional Roslyn-related extension methods should be placed in
+///
+public static class RoslynExtensions
{
- ///
- /// Source: https://github.com/dominikjeske/Samples/blob/main/SourceGenerators/HomeCenter.SourceGenerators/Extensions/RoslynExtensions.cs
- ///
- public static class RoslynExtensions
+ private static IEnumerable GetBaseTypesAndThis(this ITypeSymbol type)
{
- private static IEnumerable GetBaseTypesAndThis(this ITypeSymbol type)
- {
- var current = type;
- while (current != null)
- {
- yield return current;
- current = current.BaseType;
- }
- }
-
- public static IEnumerable GetAllMembers(this ITypeSymbol type)
+ var current = type;
+ while (current != null)
{
- return type.GetBaseTypesAndThis().SelectMany(n => n.GetMembers());
+ yield return current;
+ current = current.BaseType;
}
+ }
- public static string GetClassName(this ClassDeclarationSyntax proxy)
- {
- return proxy.Identifier.Text;
- }
-
- ///
- /// Thanks to https://www.codeproject.com/Articles/871704/Roslyn-Code-Analysis-in-Easy-Samples-Part-2
- ///
- public static string GetWhereStatement(
- this ITypeParameterSymbol typeParameterSymbol,
- SymbolDisplayFormat typeDisplayFormat,
- List generatedInterfaceNames
- )
- {
- var result = $"where {typeParameterSymbol.Name} : ";
-
- var constraints = new List();
-
- if (typeParameterSymbol.HasReferenceTypeConstraint)
- {
- constraints.Add("class");
- }
-
- if (typeParameterSymbol.HasValueTypeConstraint)
- {
- constraints.Add("struct");
- }
-
- if (typeParameterSymbol.HasNotNullConstraint)
- {
- constraints.Add("notnull");
- }
-
- constraints.AddRange(
- typeParameterSymbol.ConstraintTypes.Select(t =>
- t.ToDisplayString(typeDisplayFormat, generatedInterfaceNames)
- )
- );
-
- // The new() constraint must be last
- if (typeParameterSymbol.HasConstructorConstraint)
- {
- constraints.Add("new()");
- }
+ public static IEnumerable GetAllMembers(this ITypeSymbol type)
+ {
+ return type.GetBaseTypesAndThis().SelectMany(n => n.GetMembers());
+ }
- if (constraints.Count == 0)
- {
- return "";
- }
+ public static string GetClassName(this ClassDeclarationSyntax proxy)
+ {
+ return proxy.Identifier.Text;
+ }
- result += string.Join(", ", constraints);
+ ///
+ /// Thanks to https://www.codeproject.com/Articles/871704/Roslyn-Code-Analysis-in-Easy-Samples-Part-2
+ ///
+ public static string GetWhereStatement(
+ this ITypeParameterSymbol typeParameterSymbol,
+ SymbolDisplayFormat typeDisplayFormat
+ )
+ {
+ var result = $"where {typeParameterSymbol.Name} : ";
- return result;
- }
+ var constraints = new List();
- public static string ToDisplayString(
- this IParameterSymbol symbol,
- SymbolDisplayFormat displayFormat,
- bool nullableContextEnabled,
- List generatedInterfaceNames
- )
+ if (typeParameterSymbol.HasReferenceTypeConstraint)
{
- string? RenderTypeSymbolWithNullableAnnotation(SymbolDisplayPart part) =>
- part.Symbol is ITypeSymbol typeSymbol
- ? typeSymbol
- .WithNullableAnnotation(NullableAnnotation.Annotated)
- .ToDisplayString(displayFormat)
- : null;
-
- // Special case for reference parameters with default value null (e.g. string x = null) - the nullable
- // context isn't applied automatically, so it must be forced explicitly
- var forceNullableAnnotation =
- nullableContextEnabled
- && symbol
- is {
- Type.IsReferenceType: true,
- HasExplicitDefaultValue: true,
- ExplicitDefaultValue: null
- }
- && symbol.NullableAnnotation != NullableAnnotation.Annotated;
-
- return ToDisplayString(
- symbol,
- displayFormat,
- generatedInterfaceNames,
- forceNullableAnnotation ? RenderTypeSymbolWithNullableAnnotation : null
- );
+ constraints.Add("class");
}
- public static string ToDisplayString(
- this ITypeSymbol symbol,
- SymbolDisplayFormat displayFormat,
- List generatedInterfaceNames
- ) => ToDisplayString((ISymbol)symbol, displayFormat, generatedInterfaceNames);
-
- ///
- /// Wraps with custom resolution for generated types
- ///
- private static string ToDisplayString(
- this ISymbol symbol,
- SymbolDisplayFormat displayFormat,
- List generatedInterfaceNames,
- Func? customRenderDisplayPart = null
- )
+ if (typeParameterSymbol.HasValueTypeConstraint)
{
- var displayStringBuilder = new StringBuilder();
-
- var displayParts = GetDisplayParts(symbol, displayFormat);
-
- foreach (var part in displayParts)
- {
- if (part.Kind == SymbolDisplayPartKind.ErrorTypeName)
- {
- var unrecognisedName = part.ToString();
-
- var inferredName = ReplaceWithInferredInterfaceName(
- unrecognisedName,
- generatedInterfaceNames
- );
-
- displayStringBuilder.Append(inferredName);
- }
- else
- {
- var customRender = customRenderDisplayPart?.Invoke(part);
- displayStringBuilder.Append(customRender ?? part.ToString());
- }
- }
-
- return displayStringBuilder.ToString();
+ constraints.Add("struct");
}
- ///
- /// The same as but with adjacent SymbolDisplayParts merged into qualified type references, e.g. [Parent, ., Child] => Parent.Child
- ///
- private static IEnumerable GetDisplayParts(
- ISymbol symbol,
- SymbolDisplayFormat displayFormat
- )
+ if (typeParameterSymbol.HasNotNullConstraint)
{
- var cache = new List();
-
- foreach (var part in symbol.ToDisplayParts(displayFormat))
- {
- if (cache.Count == 0)
- {
- cache.Add(part);
- continue;
- }
-
- var previousPart = cache.Last();
-
- if (
- IsPartQualificationPunctuation(previousPart)
- ^ IsPartQualificationPunctuation(part)
- )
- {
- cache.Add(part);
- }
- else
- {
- yield return CombineQualifiedTypeParts(cache);
- cache.Clear();
- cache.Add(part);
- }
- }
-
- if (cache.Count > 0)
- {
- yield return CombineQualifiedTypeParts(cache);
- }
-
- static SymbolDisplayPart CombineQualifiedTypeParts(
- ICollection qualifiedTypeParts
- )
- {
- var qualifiedType = qualifiedTypeParts.Last();
+ constraints.Add("notnull");
+ }
- return qualifiedTypeParts.Count == 1
- ? qualifiedType
- : new SymbolDisplayPart(
- qualifiedType.Kind,
- qualifiedType.Symbol,
- string.Join("", qualifiedTypeParts)
- );
- }
+ constraints.AddRange(
+ typeParameterSymbol.ConstraintTypes.Select(t => t.ToDisplayString(typeDisplayFormat))
+ );
- static bool IsPartQualificationPunctuation(SymbolDisplayPart part) =>
- part.ToString() is "." or "::";
+ // The new() constraint must be last
+ if (typeParameterSymbol.HasConstructorConstraint)
+ {
+ constraints.Add("new()");
}
- private static string ReplaceWithInferredInterfaceName(
- string unrecognisedName,
- List generatedInterfaceNames
- )
+ if (constraints.Count == 0)
{
- var matches = generatedInterfaceNames
- .Where(name => Regex.IsMatch(name, $"[.:]{Regex.Escape(unrecognisedName)}$"))
- .ToList();
+ return "";
+ }
- if (matches.Count != 1)
- {
- // Either there's no match or an ambiguous match - we can't safely infer the interface name.
- // This is very much a "best effort" approach - if there are two interfaces with the same name,
- // there's no obvious way to work out which one the symbol is referring to.
- return unrecognisedName;
- }
+ result += string.Join(", ", constraints);
- return matches[0];
- }
+ return result;
}
}
diff --git a/AutomaticInterface/DotnetAutomaticInterface/RoslynExtensionsAutomaticInterface.cs b/AutomaticInterface/DotnetAutomaticInterface/RoslynExtensionsAutomaticInterface.cs
new file mode 100644
index 0000000..0984917
--- /dev/null
+++ b/AutomaticInterface/DotnetAutomaticInterface/RoslynExtensionsAutomaticInterface.cs
@@ -0,0 +1,218 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace DotnetAutomaticInterface;
+
+///
+/// AutomaticInterface-specific additions to
+///
+public static class RoslynExtensionsAutomaticInterface
+{
+ ///
+ /// Extended version of with support for generated interface name resolution
+ ///
+ public static string GetWhereStatement(
+ this ITypeParameterSymbol typeParameterSymbol,
+ SymbolDisplayFormat typeDisplayFormat,
+ List generatedInterfaceNames
+ )
+ {
+ var result = $"where {typeParameterSymbol.Name} : ";
+
+ var constraints = new List();
+
+ if (typeParameterSymbol.HasReferenceTypeConstraint)
+ {
+ constraints.Add("class");
+ }
+
+ if (typeParameterSymbol.HasValueTypeConstraint)
+ {
+ constraints.Add("struct");
+ }
+
+ if (typeParameterSymbol.HasNotNullConstraint)
+ {
+ constraints.Add("notnull");
+ }
+
+ constraints.AddRange(
+ typeParameterSymbol.ConstraintTypes.Select(t =>
+ t.ToDisplayString(typeDisplayFormat, generatedInterfaceNames)
+ )
+ );
+
+ // The new() constraint must be last
+ if (typeParameterSymbol.HasConstructorConstraint)
+ {
+ constraints.Add("new()");
+ }
+
+ if (constraints.Count == 0)
+ {
+ return "";
+ }
+
+ result += string.Join(", ", constraints);
+
+ return result;
+ }
+
+ public static string ToDisplayString(
+ this IParameterSymbol symbol,
+ SymbolDisplayFormat displayFormat,
+ bool nullableContextEnabled,
+ List generatedInterfaceNames
+ )
+ {
+ string? RenderTypeSymbolWithNullableAnnotation(SymbolDisplayPart part) =>
+ part.Symbol is ITypeSymbol typeSymbol
+ ? typeSymbol
+ .WithNullableAnnotation(NullableAnnotation.Annotated)
+ .ToDisplayString(displayFormat)
+ : null;
+
+ // Special case for reference parameters with default value null (e.g. string x = null) - the nullable
+ // context isn't applied automatically, so it must be forced explicitly
+ var forceNullableAnnotation =
+ nullableContextEnabled
+ && symbol
+ is {
+ Type.IsReferenceType: true,
+ HasExplicitDefaultValue: true,
+ ExplicitDefaultValue: null
+ }
+ && symbol.NullableAnnotation != NullableAnnotation.Annotated;
+
+ return ToDisplayString(
+ symbol,
+ displayFormat,
+ generatedInterfaceNames,
+ forceNullableAnnotation ? RenderTypeSymbolWithNullableAnnotation : null
+ );
+ }
+
+ public static string ToDisplayString(
+ this ITypeSymbol symbol,
+ SymbolDisplayFormat displayFormat,
+ List generatedInterfaceNames
+ ) => ToDisplayString((ISymbol)symbol, displayFormat, generatedInterfaceNames);
+
+ ///
+ /// Wraps with custom resolution for generated types
+ ///
+ private static string ToDisplayString(
+ this ISymbol symbol,
+ SymbolDisplayFormat displayFormat,
+ List generatedInterfaceNames,
+ Func? customRenderDisplayPart = null
+ )
+ {
+ var displayStringBuilder = new StringBuilder();
+
+ var displayParts = GetDisplayParts(symbol, displayFormat);
+
+ foreach (var part in displayParts)
+ {
+ if (part.Kind == SymbolDisplayPartKind.ErrorTypeName)
+ {
+ var unrecognisedName = part.ToString();
+
+ var inferredName = ReplaceWithInferredInterfaceName(
+ unrecognisedName,
+ generatedInterfaceNames
+ );
+
+ displayStringBuilder.Append(inferredName);
+ }
+ else
+ {
+ var customRender = customRenderDisplayPart?.Invoke(part);
+ displayStringBuilder.Append(customRender ?? part.ToString());
+ }
+ }
+
+ return displayStringBuilder.ToString();
+ }
+
+ ///
+ /// The same as but with adjacent SymbolDisplayParts merged into qualified type references, e.g. [Parent, ., Child] => Parent.Child
+ ///
+ private static IEnumerable GetDisplayParts(
+ ISymbol symbol,
+ SymbolDisplayFormat displayFormat
+ )
+ {
+ var cache = new List();
+
+ foreach (var part in symbol.ToDisplayParts(displayFormat))
+ {
+ if (cache.Count == 0)
+ {
+ cache.Add(part);
+ continue;
+ }
+
+ var previousPart = cache.Last();
+
+ if (IsPartQualificationPunctuation(previousPart) ^ IsPartQualificationPunctuation(part))
+ {
+ cache.Add(part);
+ }
+ else
+ {
+ yield return CombineQualifiedTypeParts(cache);
+ cache.Clear();
+ cache.Add(part);
+ }
+ }
+
+ if (cache.Count > 0)
+ {
+ yield return CombineQualifiedTypeParts(cache);
+ }
+
+ static SymbolDisplayPart CombineQualifiedTypeParts(
+ ICollection qualifiedTypeParts
+ )
+ {
+ var qualifiedType = qualifiedTypeParts.Last();
+
+ return qualifiedTypeParts.Count == 1
+ ? qualifiedType
+ : new SymbolDisplayPart(
+ qualifiedType.Kind,
+ qualifiedType.Symbol,
+ string.Join("", qualifiedTypeParts)
+ );
+ }
+
+ static bool IsPartQualificationPunctuation(SymbolDisplayPart part) =>
+ part.ToString() is "." or "::";
+ }
+
+ private static string ReplaceWithInferredInterfaceName(
+ string unrecognisedName,
+ List generatedInterfaceNames
+ )
+ {
+ var matches = generatedInterfaceNames
+ .Where(name => Regex.IsMatch(name, $"[.:]{Regex.Escape(unrecognisedName)}$"))
+ .ToList();
+
+ if (matches.Count != 1)
+ {
+ // Either there's no match or an ambiguous match - we can't safely infer the interface name.
+ // This is very much a "best effort" approach - if there are two interfaces with the same name,
+ // there's no obvious way to work out which one the symbol is referring to.
+ return unrecognisedName;
+ }
+
+ return matches[0];
+ }
+}