diff --git a/AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj b/AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj index f0b0404..87e7223 100644 --- a/AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj +++ b/AutomaticInterface/AutomaticInterface/AutomaticInterface.csproj @@ -24,11 +24,10 @@ MIT True latest-Recommended - 5.2.6 + 6.0.0 README.md true 1701;1702;NU5128 - mproves inheritdoc so that developer documentation is properly referenced on the autogenerated interfaces TRACE;DEBUGGENERATOR diff --git a/AutomaticInterface/AutomaticInterface/AutomaticInterfaceGenerator.cs b/AutomaticInterface/AutomaticInterface/AutomaticInterfaceGenerator.cs index f147501..f9c86a4 100644 --- a/AutomaticInterface/AutomaticInterface/AutomaticInterfaceGenerator.cs +++ b/AutomaticInterface/AutomaticInterface/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/AutomaticInterface/Builder.cs b/AutomaticInterface/AutomaticInterface/Builder.cs index 49893ee..3be544d 100644 --- a/AutomaticInterface/AutomaticInterface/Builder.cs +++ b/AutomaticInterface/AutomaticInterface/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; @@ -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}"; + } + + /// 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 + ) + { + 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, @@ -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() @@ -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 @@ -93,7 +131,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 +146,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; @@ -116,7 +162,13 @@ private static void AddMethod(InterfaceBuilder codeGenerator, IMethodSymbol meth 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)); @@ -124,14 +176,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 @@ -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 members, + InterfaceBuilder codeGenerator, + List 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 members, InterfaceBuilder codeGenerator) { members .Where(x => x.Kind == SymbolKind.Event) @@ -243,7 +260,7 @@ private static void AddEventsToInterface(List members, InterfaceBuilder codeGenerator.AddEventToInterface( name, - type.ToDisplayString(FullyQualifiedDisplayFormat), + type.ToDisplayString(FullyQualifiedDisplayFormat, generatedInterfaceNames), InheritDoc(evt) ); }); @@ -251,7 +268,8 @@ private static void AddEventsToInterface(List members, InterfaceBuilder private static void AddPropertiesToInterface( List members, - InterfaceBuilder interfaceGenerator + InterfaceBuilder interfaceGenerator, + List generatedInterfaceNames ) { members @@ -274,7 +292,7 @@ InterfaceBuilder interfaceGenerator interfaceGenerator.AddPropertyToInterface( name, - type.ToDisplayString(FullyQualifiedDisplayFormat), + type.ToDisplayString(FullyQualifiedDisplayFormat, generatedInterfaceNames), hasGet, hasSet, isRef, @@ -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 generatedInterfaceNames + ) { var whereStatements = typeSymbol .TypeParameters.Select(typeParameter => - typeParameter.GetWhereStatement(FullyQualifiedDisplayFormat) + typeParameter.GetWhereStatement( + FullyQualifiedDisplayFormat, + generatedInterfaceNames + ) ) .Where(constraint => !string.IsNullOrEmpty(constraint)); diff --git a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs index 6606fe4..6776397 100644 --- a/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs +++ b/AutomaticInterface/AutomaticInterface/RoslynExtensions.cs @@ -1,7 +1,8 @@ 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; @@ -37,7 +38,8 @@ public static string GetClassName(this ClassDeclarationSyntax proxy) /// public static string GetWhereStatement( this ITypeParameterSymbol typeParameterSymbol, - SymbolDisplayFormat typeDisplayFormat + SymbolDisplayFormat typeDisplayFormat, + List generatedInterfaceNames ) { var result = $"where {typeParameterSymbol.Name} : "; @@ -61,7 +63,7 @@ SymbolDisplayFormat typeDisplayFormat constraints.AddRange( typeParameterSymbol.ConstraintTypes.Select(t => - t.ToDisplayString(typeDisplayFormat) + t.ToDisplayString(typeDisplayFormat, generatedInterfaceNames) ) ); @@ -80,5 +82,161 @@ SymbolDisplayFormat typeDisplayFormat 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/Tests/Infrastructure.cs b/AutomaticInterface/Tests/Infrastructure.cs index 9a5c662..0420f83 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); @@ -45,6 +45,10 @@ 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 c65520d..c1c55fd 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.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 5e75813..18ead60 100644 --- a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs +++ b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs @@ -170,4 +170,132 @@ 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)); + } + + [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)); + } } diff --git a/README.md b/README.md index 6566726..bdee03d 100644 --- a/README.md +++ b/README.md @@ -188,9 +188,13 @@ Note that we use [Verify](https://github.com/VerifyTests/Verify) for testing. It ## Changelog +### 6.0.0 + +- Added feature to allow generated interfaces to reference other generated interfaces + ### 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