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..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; @@ -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,29 +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 ) - { - // 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) { members .Where(x => x.Kind == SymbolKind.Event) @@ -226,7 +260,7 @@ private static void AddEventsToInterface(List members, InterfaceBuilder codeGenerator.AddEventToInterface( name, - type.ToDisplayString(FullyQualifiedDisplayFormat), + type.ToDisplayString(FullyQualifiedDisplayFormat, generatedInterfaceNames), InheritDoc(evt) ); }); @@ -234,7 +268,8 @@ private static void AddEventsToInterface(List members, InterfaceBuilder private static void AddPropertiesToInterface( List members, - InterfaceBuilder interfaceGenerator + InterfaceBuilder interfaceGenerator, + List generatedInterfaceNames ) { members @@ -257,7 +292,7 @@ InterfaceBuilder interfaceGenerator interfaceGenerator.AddPropertyToInterface( name, - type.ToDisplayString(FullyQualifiedDisplayFormat), + type.ToDisplayString(FullyQualifiedDisplayFormat, generatedInterfaceNames), hasGet, hasSet, isRef, @@ -314,11 +349,18 @@ private static string GetDocumentationForClass(CSharpSyntaxNode classSyntax) return trivia.ToFullString().Trim(); } - private static string GetGeneric(TypeDeclarationSyntax classSyntax, INamedTypeSymbol typeSymbol) + private static string GetGeneric( + TypeDeclarationSyntax classSyntax, + INamedTypeSymbol typeSymbol, + List generatedInterfaceNames + ) { var whereStatements = typeSymbol .TypeParameters.Select(typeParameter => - typeParameter.GetWhereStatement(FullyQualifiedDisplayFormat) + typeParameter.GetWhereStatement( + FullyQualifiedDisplayFormat, + generatedInterfaceNames + ) ) .Where(constraint => !string.IsNullOrEmpty(constraint)); 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/Infrastructure.cs b/AutomaticInterface/Tests/Infrastructure.cs index 2fd51f7..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); @@ -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 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/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); } } diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedGenericInterfaceReferences.verified.txt b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithGeneratedGenericInterfaceReferences.verified.txt new file mode 100644 index 0000000..68d59c1 --- /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("DotnetAutomaticInterface", "")] + 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("DotnetAutomaticInterface", "")] + 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..d1ea24d --- /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("DotnetAutomaticInterface", "")] + 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("DotnetAutomaticInterface", "")] + public partial interface IModel + { + } +} diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferences.verified.txt b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.WorksWithQualifiedGeneratedInterfaceReferences.verified.txt new file mode 100644 index 0000000..0cab8af --- /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("DotnetAutomaticInterface", "")] + 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("DotnetAutomaticInterface", "")] + 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..de68055 --- /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("DotnetAutomaticInterface", "")] + 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("DotnetAutomaticInterface", "")] + public partial interface IModel + { + } +} diff --git a/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs b/AutomaticInterface/Tests/TypeResolutions/TypeResolutions.cs index b67944d..fa5bff9 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 DotnetAutomaticInterface; + + 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 DotnetAutomaticInterface; + 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 DotnetAutomaticInterface; + + 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 DotnetAutomaticInterface; + + 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 aa9adb0..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. @@ -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