diff --git a/.gitignore b/.gitignore index dfcfd56..7508c6d 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,4 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ +/tests/ProxyInterfaceSourceGeneratorTests/Destination/Disposable/*.g.cs diff --git a/ProxyInterfaceSourceGenerator Solution.sln b/ProxyInterfaceSourceGenerator Solution.sln index cb4388c..2da966b 100644 --- a/ProxyInterfaceSourceGenerator Solution.sln +++ b/ProxyInterfaceSourceGenerator Solution.sln @@ -14,6 +14,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution PackageReleaseNotes.txt = PackageReleaseNotes.txt README.md = README.md ReleaseNotes.md = ReleaseNotes.md + ToDo.md = ToDo.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{19009F5B-3267-45E2-A8B6-89F2AB47D72C}" diff --git a/ToDo.md b/ToDo.md new file mode 100644 index 0000000..5f7957a --- /dev/null +++ b/ToDo.md @@ -0,0 +1,5 @@ +# Ideas for further improvements + +## Replaced out parameters +## Replaced Events +## Generate explicit interface code \ No newline at end of file diff --git a/src/ProxyInterfaceSourceGenerator/Extensions/NamedTypeSymbolExtensions.cs b/src/ProxyInterfaceSourceGenerator/Extensions/NamedTypeSymbolExtensions.cs index ad53355..dc37dcb 100644 --- a/src/ProxyInterfaceSourceGenerator/Extensions/NamedTypeSymbolExtensions.cs +++ b/src/ProxyInterfaceSourceGenerator/Extensions/NamedTypeSymbolExtensions.cs @@ -60,4 +60,41 @@ internal static class NamedTypeSymbolExtensions $"{namedTypeSymbol}Proxy" : $"{namedTypeSymbol}Proxy<{string.Join(", ", namedTypeSymbol.TypeArguments.Select(ta => ta.Name))}>"; } + + public static List ResolveImplementedInterfaces(this INamedTypeSymbol symbol, bool proxyBaseClasses) + { + //Members implemented by us or base classes should go here. + var publicMembers = symbol.GetMembers().Where(m => m.DeclaredAccessibility == Accessibility.Public).ToList(); + //Direct interfaces, recursive interfaces or base class interfaces should go here. + var interfaces = new List(symbol.Interfaces); + var baseType = symbol.BaseType; + while (proxyBaseClasses && baseType != null && baseType.SpecialType != SpecialType.System_Object) + { + publicMembers.AddRange(baseType.GetMembers().Where(m => m.DeclaredAccessibility == Accessibility.Public)); + interfaces.AddRange(baseType.Interfaces); + baseType = baseType.BaseType; + } + + //Filter explicitly implemented interfaces. + var realizedInterfaces = new List(); + foreach (var iface in interfaces) + { + var isRealized = true; + var allMembers = iface.AllInterfaces.Aggregate(iface.GetMembers(), (xs, x) => xs.AddRange(x.GetMembers())); + foreach (var member in allMembers) + { + var implementation = symbol.FindImplementationForInterfaceMember(member); + if (!publicMembers.Contains(implementation!)) + { + isRealized = false; + break; + } + } + if (isRealized) + { + realizedInterfaces.Add(iface); + } + } + return realizedInterfaces; + } } \ No newline at end of file diff --git a/src/ProxyInterfaceSourceGenerator/FileGenerators/PartialInterfacesGenerator.cs b/src/ProxyInterfaceSourceGenerator/FileGenerators/PartialInterfacesGenerator.cs index 11eba62..20f0b00 100644 --- a/src/ProxyInterfaceSourceGenerator/FileGenerators/PartialInterfacesGenerator.cs +++ b/src/ProxyInterfaceSourceGenerator/FileGenerators/PartialInterfacesGenerator.cs @@ -11,6 +11,7 @@ namespace ProxyInterfaceSourceGenerator.FileGenerators; internal class PartialInterfacesGenerator : BaseGenerator, IFilesGenerator { + private IReadOnlyCollection ImplementedInterfaces = new List(); public PartialInterfacesGenerator(Context context, bool supportsNullable) : base(context, supportsNullable) { @@ -58,6 +59,9 @@ internal class PartialInterfacesGenerator : BaseGenerator, IFilesGenerator ProxyData proxyData) { var extendsProxyClasses = GetExtendsProxyData(proxyData, classSymbol); + ImplementedInterfaces = classSymbol.Symbol.ResolveImplementedInterfaces(proxyData.ProxyBaseClasses); + var implementedInterfacesNames = ImplementedInterfaces.Select(i => i.ToDisplayString(NullableFlowState.None, SymbolDisplayFormat.FullyQualifiedFormat)); + var implements = implementedInterfacesNames.Any() ? $" : {string.Join(", ", implementedInterfacesNames)}" : string.Empty; var @new = extendsProxyClasses.Any() ? "new " : string.Empty; var (namespaceStart, namespaceEnd) = NamespaceBuilder.Build(ns); @@ -74,7 +78,7 @@ internal class PartialInterfacesGenerator : BaseGenerator, IFilesGenerator using System; {namespaceStart} - public partial interface {interfaceName} + public partial interface {interfaceName}{implements} {{ {@new}{classSymbol.Symbol} _Instance {{ get; }} @@ -88,11 +92,26 @@ using System; {SupportsNullable.IIf("#nullable restore")}"; } + private Func InterfaceFilter() where T : ISymbol + { + var hashSet = new HashSet(); + foreach (var iface in ImplementedInterfaces) + { + var members = iface.AllInterfaces.Aggregate(iface.GetMembers(), (xs, x) => xs.AddRange(x.GetMembers())); + foreach (var member in members) + { + hashSet.Add(member.Name); + } + } + //Member is not already implemented in another interface. + return (T t) => !hashSet.Contains(t.Name); + } + private string GenerateProperties(ClassSymbol targetClassSymbol, bool proxyBaseClasses) { var str = new StringBuilder(); - foreach (var property in MemberHelper.GetPublicProperties(targetClassSymbol, proxyBaseClasses)) + foreach (var property in MemberHelper.GetPublicProperties(targetClassSymbol, proxyBaseClasses, InterfaceFilter())) { var type = GetPropertyType(property, out var isReplaced); @@ -125,7 +144,7 @@ using System; private string GenerateMethods(ClassSymbol targetClassSymbol, bool proxyBaseClasses) { var str = new StringBuilder(); - foreach (var method in MemberHelper.GetPublicMethods(targetClassSymbol, proxyBaseClasses)) + foreach (var method in MemberHelper.GetPublicMethods(targetClassSymbol, proxyBaseClasses, InterfaceFilter())) { var methodParameters = GetMethodParameters(method.Parameters, true); var whereStatement = GetWhereStatementFromMethod(method); @@ -145,7 +164,7 @@ using System; private string GenerateEvents(ClassSymbol targetClassSymbol, bool proxyBaseClasses) { var str = new StringBuilder(); - foreach (var @event in MemberHelper.GetPublicEvents(targetClassSymbol, proxyBaseClasses)) + foreach (var @event in MemberHelper.GetPublicEvents(targetClassSymbol, proxyBaseClasses, InterfaceFilter())) { var ps = @event.First().Parameters.First(); var type = ps.GetTypeEnum() == TypeEnum.Complex ? GetParameterType(ps, out _) : ps.Type.ToString(); diff --git a/src/ProxyInterfaceSourceGenerator/FileGenerators/ProxyClassesGenerator.cs b/src/ProxyInterfaceSourceGenerator/FileGenerators/ProxyClassesGenerator.cs index 991c013..53f66cb 100644 --- a/src/ProxyInterfaceSourceGenerator/FileGenerators/ProxyClassesGenerator.cs +++ b/src/ProxyInterfaceSourceGenerator/FileGenerators/ProxyClassesGenerator.cs @@ -82,7 +82,7 @@ internal partial class ProxyClassesGenerator : BaseGenerator, IFilesGenerator var operators = GenerateOperators(targetClassSymbol, pd.ProxyBaseClasses); var configurationForMapster = string.Empty; - if (Context.ReplacedTypes.Any()) + if (Context.ReplacedTypes.Count > 0) { configurationForMapster = GenerateMapperConfigurationForMapster(); } diff --git a/tests/ProxyInterfaceSourceGeneratorTests/InheritedInterfaceTests.cs b/tests/ProxyInterfaceSourceGeneratorTests/InheritedInterfaceTests.cs new file mode 100644 index 0000000..05bd250 --- /dev/null +++ b/tests/ProxyInterfaceSourceGeneratorTests/InheritedInterfaceTests.cs @@ -0,0 +1,164 @@ +using CSharp.SourceGenerators.Extensions; +using CSharp.SourceGenerators.Extensions.Models; +using FluentAssertions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ProxyInterfaceSourceGenerator; +using ProxyInterfaceSourceGeneratorTests.Source.Disposable; +using Xunit.Abstractions; + +namespace ProxyInterfaceSourceGeneratorTests; + +public class InheritedInterfaceTests +{ + private const string Namespace = "ProxyInterfaceSourceGeneratorTests.Source.Disposable"; + private const string OutputPath = "../../../Destination/Disposable/"; + private readonly ProxyInterfaceCodeGenerator _sut; + + public InheritedInterfaceTests() + { + if (!Directory.Exists(OutputPath)) + { + Directory.CreateDirectory(OutputPath); + } + _sut = new ProxyInterfaceCodeGenerator(); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, true)] + public void GenerateFiles_InheritedInterface_InheritFromBaseClass(bool proxyBaseClass, bool inheritBaseInterface) + { + var name = "Child"; + var interfaceName = "I" + name; + var proxyName = name + "Proxy"; + + // Arrange + string[] fileNames = [ + $"{Namespace}.{interfaceName}.g.cs", + $"{Namespace}.{proxyName}.g.cs" + ]; + var path = $"./Source/Disposable/{interfaceName}.cs"; + SourceFile sourceFile = CreateSourceFile(path, name, proxyBaseClass); + + // Act + var result = _sut.Execute([sourceFile]); + + result.Valid.Should().BeTrue(); + result.Files.Should().HaveCount(fileNames.Length + 1); + WriteFiles(fileNames, result); + + var interfaceIndex = 1; + var tree = result.Files[interfaceIndex].SyntaxTree; + var root = tree.GetRoot(); + var interfaceDeclarations = root.DescendantNodes().OfType(); + + // Assert + Assert.Single(interfaceDeclarations); + var baseList = interfaceDeclarations.First().BaseList; + bool didWeInherit = baseList is not null; + Assert.Equal(didWeInherit, inheritBaseInterface); + } + + [Theory] + [InlineData("Parent")] + [InlineData("Child")] + public void GenerateFiles_InheritedInterface_Should_InheritTheInterface(string name) + { + var interfaceName = "I" + name; + var proxyName = name + "Proxy"; + + // Arrange + string[] fileNames = [ + $"{Namespace}.{interfaceName}.g.cs", + $"{Namespace}.{proxyName}.g.cs" + ]; + + var path = $"./Source/Disposable/{interfaceName}.cs"; + SourceFile sourceFile = CreateSourceFile(path, name, true); + + // Act + var result = _sut.Execute([sourceFile]); + + result.Valid.Should().BeTrue(); + result.Files.Should().HaveCount(fileNames.Length + 1); + WriteFiles(fileNames, result); + + var interfaceIndex = 1; + var tree = result.Files[interfaceIndex].SyntaxTree; + var root = tree.GetRoot(); + var interfaceDeclarations = root.DescendantNodes().OfType(); + + // Assert + Assert.Single(interfaceDeclarations); + var baseList = interfaceDeclarations.First().BaseList!; + Assert.Equal(2, baseList.Types.Count); + var type1 = (QualifiedNameSyntax)baseList.Types[0].Type; + var type2 = (QualifiedNameSyntax)baseList.Types[1].Type; + Assert.Equal(nameof(IDisposable), type1.Right.Identifier.Text); + Assert.Equal(nameof(IUpdate), type2.Right.Identifier.Text); + } + + [Fact] + public void GenerateFiles_InheritedInterface_Should_Not_InheritExplicitImplementedInterfaces() + { + var name = "Explicit"; + var interfaceName = "I" + name; + var proxyName = name + "Proxy"; + + // Arrange + string[] fileNames = [ + $"{Namespace}.{interfaceName}.g.cs", + $"{Namespace}.{proxyName}.g.cs" + ]; + var interfaceIndex = 1; + var path = $"./Source/Disposable/{interfaceName}.cs"; + SourceFile sourceFile = CreateSourceFile(path, name, true); + + // Act + var result = _sut.Execute([sourceFile]); + + result.Valid.Should().BeTrue(); + result.Files.Should().HaveCount(fileNames.Length + 1); + WriteFiles(fileNames, result); + + var tree = result.Files[interfaceIndex].SyntaxTree; + var root = tree.GetRoot(); + var interfaceDeclarations = root.DescendantNodes().OfType(); + + // Assert + //This actually could work, we just need to implenent the logic inside the Proxy (and interface). + //⚠ Dispose is not a public member of the 'Explicit' class and also not of the Proxy. + //e.g. new Explicit().Dipose() is not possible. + Assert.Single(interfaceDeclarations); + var baseList = interfaceDeclarations.First().BaseList; + bool noInterfaceImplementationFound = baseList is null; + Assert.True(noInterfaceImplementationFound); + } + + private static SourceFile CreateSourceFile(string path, string name, bool extend) + { + var extendString = extend.ToString().ToLowerInvariant(); + return new SourceFile + { + Path = path, + Text = File.ReadAllText(path), + AttributeToAddToInterface = new ExtraAttribute + { + Name = "ProxyInterfaceGenerator.Proxy", + ArgumentList = $"typeof({Namespace}.{name}), {extendString}" + } + }; + } + + private static void WriteFiles(string[] fileNames, ExecuteResult result) + { + foreach (var fileName in fileNames.Select((fileName, index) => new { fileName, index })) + { + var builder = result.Files[fileName.index + 1]; // +1 means skip the attribute + builder.Path.Should().EndWith(fileName.fileName); + File.WriteAllText($"{OutputPath}{fileName.fileName}", builder.Text); + builder.Text.Should().Be(File.ReadAllText($"{OutputPath}{fileName.fileName}")); + } + } +} \ No newline at end of file diff --git a/tests/ProxyInterfaceSourceGeneratorTests/ProxyInterfaceSourceGeneratorTests.csproj b/tests/ProxyInterfaceSourceGeneratorTests/ProxyInterfaceSourceGeneratorTests.csproj index 5a6fb04..c9156d3 100644 --- a/tests/ProxyInterfaceSourceGeneratorTests/ProxyInterfaceSourceGeneratorTests.csproj +++ b/tests/ProxyInterfaceSourceGeneratorTests/ProxyInterfaceSourceGeneratorTests.csproj @@ -1,9 +1,9 @@ - net6.0 + net8.0 false - 10.0 + 12.0 enable Debug;Release;DebugAttach enable @@ -52,10 +52,9 @@ - - - - + + PreserveNewest + PreserveNewest diff --git a/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/Child.cs b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/Child.cs new file mode 100644 index 0000000..ae33ecf --- /dev/null +++ b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/Child.cs @@ -0,0 +1,6 @@ +namespace ProxyInterfaceSourceGeneratorTests.Source.Disposable +{ + public class Child : Parent + { + } +} \ No newline at end of file diff --git a/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/Explicit.cs b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/Explicit.cs new file mode 100644 index 0000000..0ba143e --- /dev/null +++ b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/Explicit.cs @@ -0,0 +1,25 @@ +namespace ProxyInterfaceSourceGeneratorTests.Source.Disposable +{ + public class Explicit : IDisposable, IUpdate + { + string IUpdate.Name => throw new NotSupportedException(); + + event EventHandler? IUpdate.Update + { + add + { + throw new NotSupportedException(); + } + + remove + { + throw new NotSupportedException(); + } + } + + void IDisposable.Dispose() + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/IChild.cs b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/IChild.cs new file mode 100644 index 0000000..dd50972 --- /dev/null +++ b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/IChild.cs @@ -0,0 +1,6 @@ +namespace ProxyInterfaceSourceGeneratorTests.Source.Disposable +{ + public partial interface IChild + { + } +} \ No newline at end of file diff --git a/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/IExplicit.cs b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/IExplicit.cs new file mode 100644 index 0000000..4bfc5b9 --- /dev/null +++ b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/IExplicit.cs @@ -0,0 +1,6 @@ +namespace ProxyInterfaceSourceGeneratorTests.Source.Disposable +{ + public partial interface IExplicit + { + } +} \ No newline at end of file diff --git a/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/IParent.cs b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/IParent.cs new file mode 100644 index 0000000..d5e2fb1 --- /dev/null +++ b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/IParent.cs @@ -0,0 +1,6 @@ +namespace ProxyInterfaceSourceGeneratorTests.Source.Disposable +{ + public partial interface IParent + { + } +} \ No newline at end of file diff --git a/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/IUpdate.cs b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/IUpdate.cs new file mode 100644 index 0000000..6ace218 --- /dev/null +++ b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/IUpdate.cs @@ -0,0 +1,9 @@ +namespace ProxyInterfaceSourceGeneratorTests.Source.Disposable +{ + public interface IUpdate + { + event EventHandler? Update; + + string Name { get; } + } +} \ No newline at end of file diff --git a/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/Parent.cs b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/Parent.cs new file mode 100644 index 0000000..f17308d --- /dev/null +++ b/tests/ProxyInterfaceSourceGeneratorTests/Source/Disposable/Parent.cs @@ -0,0 +1,45 @@ +namespace ProxyInterfaceSourceGeneratorTests.Source.Disposable +{ + public class Parent : IDisposable, IUpdate + { + private bool disposedValue; + + public event EventHandler? Update; + + public string Name => nameof(Parent); + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public bool Empty() + { + return false; + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TrashCan() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + } +} \ No newline at end of file