From 7c68455a8ebfc86d559fa52ac9426250c255ccc7 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 23 Jul 2021 16:28:11 +0200 Subject: [PATCH] initial code --- ProxyInterfaceSourceGenerator Solution.sln | 46 +++++++ .../ProxyInterfaceConsumer/IPerson.cs | 7 + src-examples/ProxyInterfaceConsumer/Person.cs | 33 +++++ .../ProxyInterfaceConsumer/Program.cs | 11 ++ .../ProxyInterfaceConsumer.csproj | 21 +++ .../Extensions/SymbolExtensions.cs | 27 ++++ .../FileGenerators/Data.cs | 9 ++ .../FileGenerators/IFileGenerator.cs | 7 + .../FileGenerators/IFilesGenerator.cs | 9 ++ .../PartialInterfacesGenerator.cs | 72 ++++++++++ .../FileGenerators/ProxyAttributeGenerator.cs | 30 ++++ .../ProxyInterfaceCodeGenerator.cs | 130 ++++++++++++++++++ .../ProxyInterfaceSourceGenerator.csproj | 17 +++ .../ProxySyntaxReceiver.cs | 42 ++++++ .../Utils/MemberHelper.cs | 66 +++++++++ 15 files changed, 527 insertions(+) create mode 100644 ProxyInterfaceSourceGenerator Solution.sln create mode 100644 src-examples/ProxyInterfaceConsumer/IPerson.cs create mode 100644 src-examples/ProxyInterfaceConsumer/Person.cs create mode 100644 src-examples/ProxyInterfaceConsumer/Program.cs create mode 100644 src-examples/ProxyInterfaceConsumer/ProxyInterfaceConsumer.csproj create mode 100644 src/ProxyInterfaceSourceGenerator/Extensions/SymbolExtensions.cs create mode 100644 src/ProxyInterfaceSourceGenerator/FileGenerators/Data.cs create mode 100644 src/ProxyInterfaceSourceGenerator/FileGenerators/IFileGenerator.cs create mode 100644 src/ProxyInterfaceSourceGenerator/FileGenerators/IFilesGenerator.cs create mode 100644 src/ProxyInterfaceSourceGenerator/FileGenerators/PartialInterfacesGenerator.cs create mode 100644 src/ProxyInterfaceSourceGenerator/FileGenerators/ProxyAttributeGenerator.cs create mode 100644 src/ProxyInterfaceSourceGenerator/ProxyInterfaceCodeGenerator.cs create mode 100644 src/ProxyInterfaceSourceGenerator/ProxyInterfaceSourceGenerator.csproj create mode 100644 src/ProxyInterfaceSourceGenerator/ProxySyntaxReceiver.cs create mode 100644 src/ProxyInterfaceSourceGenerator/Utils/MemberHelper.cs diff --git a/ProxyInterfaceSourceGenerator Solution.sln b/ProxyInterfaceSourceGenerator Solution.sln new file mode 100644 index 0000000..16fd633 --- /dev/null +++ b/ProxyInterfaceSourceGenerator Solution.sln @@ -0,0 +1,46 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31512.422 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{ED3DA9DD-1E07-444B-A2D7-2DBA280F96D4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2CE637DC-E8F5-4603-8B57-E51A32F631F1}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{19009F5B-3267-45E2-A8B6-89F2AB47D72C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src-examples", "src-examples", "{38BA087F-EDA1-4F8A-A140-85B84791B815}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProxyInterfaceSourceGenerator", "src\ProxyInterfaceSourceGenerator\ProxyInterfaceSourceGenerator.csproj", "{C880D4B0-1E9E-4449-AEF8-0A93FB2EDFAD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProxyInterfaceConsumer", "src-examples\ProxyInterfaceConsumer\ProxyInterfaceConsumer.csproj", "{7E0A10EE-CCC3-4281-9541-B0AF037D3DF9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C880D4B0-1E9E-4449-AEF8-0A93FB2EDFAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C880D4B0-1E9E-4449-AEF8-0A93FB2EDFAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C880D4B0-1E9E-4449-AEF8-0A93FB2EDFAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C880D4B0-1E9E-4449-AEF8-0A93FB2EDFAD}.Release|Any CPU.Build.0 = Release|Any CPU + {7E0A10EE-CCC3-4281-9541-B0AF037D3DF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E0A10EE-CCC3-4281-9541-B0AF037D3DF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E0A10EE-CCC3-4281-9541-B0AF037D3DF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E0A10EE-CCC3-4281-9541-B0AF037D3DF9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C880D4B0-1E9E-4449-AEF8-0A93FB2EDFAD} = {ED3DA9DD-1E07-444B-A2D7-2DBA280F96D4} + {7E0A10EE-CCC3-4281-9541-B0AF037D3DF9} = {38BA087F-EDA1-4F8A-A140-85B84791B815} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {585F071D-051D-441C-9C6B-226D9E15A1F5} + EndGlobalSection +EndGlobal diff --git a/src-examples/ProxyInterfaceConsumer/IPerson.cs b/src-examples/ProxyInterfaceConsumer/IPerson.cs new file mode 100644 index 0000000..08336e2 --- /dev/null +++ b/src-examples/ProxyInterfaceConsumer/IPerson.cs @@ -0,0 +1,7 @@ +namespace SourceGeneratorInterface +{ + [ProxyInterfaceGenerator.Proxy(typeof(SourceGeneratorInterface.Person))] + public partial interface IPerson + { + } +} \ No newline at end of file diff --git a/src-examples/ProxyInterfaceConsumer/Person.cs b/src-examples/ProxyInterfaceConsumer/Person.cs new file mode 100644 index 0000000..363e59a --- /dev/null +++ b/src-examples/ProxyInterfaceConsumer/Person.cs @@ -0,0 +1,33 @@ +namespace SourceGeneratorInterface +{ + public class Person + { + + private int PrivateId { get; } + public int Id { get; } + + public long? NullableLong { get; } + + public string Name { get; set; } + + public Address Address { get; set; } + + public E E { get; set; } + + public int Add(string s) + { + return 600; + } + } + + public class Address + { + public int X { get; } + } + + public enum E + { + V1, + V2 + } +} \ No newline at end of file diff --git a/src-examples/ProxyInterfaceConsumer/Program.cs b/src-examples/ProxyInterfaceConsumer/Program.cs new file mode 100644 index 0000000..5a82864 --- /dev/null +++ b/src-examples/ProxyInterfaceConsumer/Program.cs @@ -0,0 +1,11 @@ +namespace SourceGeneratorInterface +{ + public class Program + { + public static void Main() + { + IPerson ip = null; + //var i = ip.Id; + } + } +} diff --git a/src-examples/ProxyInterfaceConsumer/ProxyInterfaceConsumer.csproj b/src-examples/ProxyInterfaceConsumer/ProxyInterfaceConsumer.csproj new file mode 100644 index 0000000..c914ec4 --- /dev/null +++ b/src-examples/ProxyInterfaceConsumer/ProxyInterfaceConsumer.csproj @@ -0,0 +1,21 @@ + + + + net5.0 + Exe + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + \ No newline at end of file diff --git a/src/ProxyInterfaceSourceGenerator/Extensions/SymbolExtensions.cs b/src/ProxyInterfaceSourceGenerator/Extensions/SymbolExtensions.cs new file mode 100644 index 0000000..645cbc7 --- /dev/null +++ b/src/ProxyInterfaceSourceGenerator/Extensions/SymbolExtensions.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace ProxyInterfaceSourceGenerator.Extensions +{ + internal static class SymbolExtensions + { + public static string ToCode(this IPropertySymbol property) + { + string get = property.GetMethod != null ? "get; " : string.Empty; + string set = property.SetMethod != null ? "set; " : string.Empty; + + return $"{property.Type} {property.Name} {{ {get}{set}}}"; + } + + public static string ToCode(this IMethodSymbol method) + { + var parameters = new List(); + foreach (var ps in method.Parameters) + { + parameters.Add($"{ps.Type} {ps.Name}"); + } + + return $"{method.ReturnType} {method.Name}({string.Join(", ", parameters)});"; + } + } +} diff --git a/src/ProxyInterfaceSourceGenerator/FileGenerators/Data.cs b/src/ProxyInterfaceSourceGenerator/FileGenerators/Data.cs new file mode 100644 index 0000000..19f522c --- /dev/null +++ b/src/ProxyInterfaceSourceGenerator/FileGenerators/Data.cs @@ -0,0 +1,9 @@ +namespace ProxyInterfaceSourceGenerator.FileGenerators +{ + internal record Data + { + public string FileName { get; set; } + + public string Text { get; set; } + } +} \ No newline at end of file diff --git a/src/ProxyInterfaceSourceGenerator/FileGenerators/IFileGenerator.cs b/src/ProxyInterfaceSourceGenerator/FileGenerators/IFileGenerator.cs new file mode 100644 index 0000000..37c084a --- /dev/null +++ b/src/ProxyInterfaceSourceGenerator/FileGenerators/IFileGenerator.cs @@ -0,0 +1,7 @@ +namespace ProxyInterfaceSourceGenerator.FileGenerators +{ + internal interface IFileGenerator + { + Data GenerateFile(); + } +} \ No newline at end of file diff --git a/src/ProxyInterfaceSourceGenerator/FileGenerators/IFilesGenerator.cs b/src/ProxyInterfaceSourceGenerator/FileGenerators/IFilesGenerator.cs new file mode 100644 index 0000000..78a52fc --- /dev/null +++ b/src/ProxyInterfaceSourceGenerator/FileGenerators/IFilesGenerator.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace ProxyInterfaceSourceGenerator.FileGenerators +{ + internal interface IFilesGenerator + { + IEnumerable GenerateFiles(); + } +} \ No newline at end of file diff --git a/src/ProxyInterfaceSourceGenerator/FileGenerators/PartialInterfacesGenerator.cs b/src/ProxyInterfaceSourceGenerator/FileGenerators/PartialInterfacesGenerator.cs new file mode 100644 index 0000000..2e4e248 --- /dev/null +++ b/src/ProxyInterfaceSourceGenerator/FileGenerators/PartialInterfacesGenerator.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ProxyInterfaceSourceGenerator.Extensions; +using ProxyInterfaceSourceGenerator.FileGenerators; +using ProxyInterfaceSourceGenerator.Utils; + +namespace ClassLibrarySourceGen +{ + internal class PartialInterfacesGenerator : IFilesGenerator + { + private readonly GeneratorExecutionContext _context; + private readonly IDictionary _candidateInterfaces; + + public PartialInterfacesGenerator(GeneratorExecutionContext context, IDictionary candidateInterfaces) + { + _context = context; + _candidateInterfaces = candidateInterfaces; + } + + public IEnumerable GenerateFiles() + { + foreach (var x in _candidateInterfaces) + { + string interfaceName = $"I{x.Value.Split('.').Last()}"; + yield return new Data + { + FileName = $"I{interfaceName}.cs", + Text = CreatePartialInterfaceCode(_context.Compilation.GetTypeByMetadataName(x.Value), interfaceName) + }; + } + } + + private string CreatePartialInterfaceCode(INamedTypeSymbol symbol, string interfaceName) => $@"using System; + +namespace {symbol.ContainingNamespace} +{{ + public partial interface {interfaceName} + {{ +{GenerateSimpleProperties(symbol)} + +{GenerateMethods(symbol)} + }} +}}"; + + private string GenerateSimpleProperties(INamedTypeSymbol symbol) + { + var str = new StringBuilder(); + foreach (IPropertySymbol property in MemberHelper.GetPublicProperties(symbol, p => p.Type.IsValueType || p.Type.ToString() == "string")) + { + str.AppendLine($" {property.ToCode()}"); + str.AppendLine(); + } + + return str.ToString(); + } + + private string GenerateMethods(INamedTypeSymbol symbol) + { + var str = new StringBuilder(); + foreach (IMethodSymbol method in MemberHelper.GetPublicMethods(symbol)) + { + str.AppendLine($" {method.ToCode()}"); + str.AppendLine(); + } + + return str.ToString(); + } + } +} \ No newline at end of file diff --git a/src/ProxyInterfaceSourceGenerator/FileGenerators/ProxyAttributeGenerator.cs b/src/ProxyInterfaceSourceGenerator/FileGenerators/ProxyAttributeGenerator.cs new file mode 100644 index 0000000..763fc6b --- /dev/null +++ b/src/ProxyInterfaceSourceGenerator/FileGenerators/ProxyAttributeGenerator.cs @@ -0,0 +1,30 @@ +namespace ProxyInterfaceSourceGenerator.FileGenerators +{ + internal class ProxyAttributeGenerator : IFileGenerator + { + private const string ClassName = "ProxyAttribute"; + + public Data GenerateFile() + { + return new Data + { + FileName = $"{ClassName}.cs", + Text = $@"using System; + +namespace ProxyInterfaceGenerator +{{ + [AttributeUsage(AttributeTargets.Interface)] + public class {ClassName} : Attribute + {{ + public Type Type {{ get; }} + + public {ClassName}(Type type) + {{ + Type = type; + }} + }} +}}" + }; + } + } +} \ No newline at end of file diff --git a/src/ProxyInterfaceSourceGenerator/ProxyInterfaceCodeGenerator.cs b/src/ProxyInterfaceSourceGenerator/ProxyInterfaceCodeGenerator.cs new file mode 100644 index 0000000..15e9376 --- /dev/null +++ b/src/ProxyInterfaceSourceGenerator/ProxyInterfaceCodeGenerator.cs @@ -0,0 +1,130 @@ +using System; +using System.Diagnostics; +using System.Text; +using ClassLibrarySourceGen; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using ProxyInterfaceSourceGenerator.FileGenerators; + +namespace ProxyInterfaceSourceGenerator +{ + [Generator] + public class ProxyInterfaceCodeGenerator : ISourceGenerator + { + private readonly ProxyAttributeGenerator _proxyAttributeGenerator = new ProxyAttributeGenerator(); + + public void Initialize(GeneratorInitializationContext context) + { + if (!Debugger.IsAttached) + { + Debugger.Launch(); + } + + context.RegisterForSyntaxNotifications(() => new ProxySyntaxReceiver()); + } + public void Execute(GeneratorExecutionContext context) + { + var attributeData = _proxyAttributeGenerator.GenerateFile(); + context.AddSource(attributeData.FileName, SourceText.From(attributeData.Text, Encoding.UTF8)); + + if (context.SyntaxReceiver is not ProxySyntaxReceiver receiver) + { + return; + } + + var p = context.Compilation.GetTypeByMetadataName("SourceGeneratorInterface.Person"); + var ec = context.Compilation.GetTypeByMetadataName("Microsoft.CodeAnalysis.GeneratorExecutionContext"); + + var generator = new PartialInterfacesGenerator(context, receiver.CandidateInterfaces); + + foreach (var data in generator.GenerateFiles()) + { + context.AddSource(data.FileName, SourceText.From(data.Text, Encoding.UTF8)); + } + } + } + + public struct Test + { + public int Id { get; set; } + + public Clazz C { get; } + } + + public sealed class Clazz + { + public string Name { get; set; } + } + + public interface ITest + { + int Id { get; set; } + + IClazz C { get; } + } + + public interface IClazz + { + string Name { get; set; } + } + + public class TestMock : ITest + { + private Test _instance; + + private IClazz _clazz; + + public TestMock(Test instance) + { + _instance = instance; + + _clazz = new ClazzMock(_instance.C); + } + + public int Id + { + get => _instance.Id; + set => _instance.Id = value; + } + + public IClazz C => _clazz; + } + + public class ClazzMock : IClazz + { + private Clazz _instance; + + public ClazzMock(Clazz instance) + { + _instance = instance; + } + + public string Name + { + get => _instance.Name; + set => _instance.Name = value; + } + } + + public interface IPrintable + { + int Length { get; } + int Count { get; } + void Print1(); + void Print2(); + } + + public class PrinterV1 : IPrintable + { + public int Length => 100; + public int Count => 200; + public void Print1() + { + Console.WriteLine("PrinterV1 - Print1"); + } + public void Print2() + { + Console.WriteLine("PrinterV1 - Print2"); + } + } +} \ No newline at end of file diff --git a/src/ProxyInterfaceSourceGenerator/ProxyInterfaceSourceGenerator.csproj b/src/ProxyInterfaceSourceGenerator/ProxyInterfaceSourceGenerator.csproj new file mode 100644 index 0000000..39620e9 --- /dev/null +++ b/src/ProxyInterfaceSourceGenerator/ProxyInterfaceSourceGenerator.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + 9 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + \ No newline at end of file diff --git a/src/ProxyInterfaceSourceGenerator/ProxySyntaxReceiver.cs b/src/ProxyInterfaceSourceGenerator/ProxySyntaxReceiver.cs new file mode 100644 index 0000000..44de8e3 --- /dev/null +++ b/src/ProxyInterfaceSourceGenerator/ProxySyntaxReceiver.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace ProxyInterfaceSourceGenerator +{ + internal class ProxySyntaxReceiver : ISyntaxReceiver + { + public IDictionary CandidateInterfaces { get; } = new Dictionary(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is InterfaceDeclarationSyntax interfaceDeclarationSyntax && TryGet(interfaceDeclarationSyntax, out string typeName)) + { + CandidateInterfaces.Add(interfaceDeclarationSyntax, typeName); + } + } + + private static bool TryGet(InterfaceDeclarationSyntax interfaceDeclarationSyntax, out string typeName) + { + typeName = null; + + // TODO : how to check if the InterfaceDeclarationSyntax has 'partial' ? + var attrinbuteLists = interfaceDeclarationSyntax.AttributeLists.FirstOrDefault(x => x.Attributes.Any(a => a.Name.ToString().Equals("ProxyInterfaceGenerator.Proxy"))); + if (attrinbuteLists == null) + { + return false; + } + + var attributeSyntax = attrinbuteLists.Attributes.FirstOrDefault(); + if (attributeSyntax == null) + { + return false; + } + + var arg = attributeSyntax.ArgumentList.Arguments.First(); + typeName = arg.Expression.ChildNodes().First().GetText().ToString(); + return true; + } + } +} diff --git a/src/ProxyInterfaceSourceGenerator/Utils/MemberHelper.cs b/src/ProxyInterfaceSourceGenerator/Utils/MemberHelper.cs new file mode 100644 index 0000000..58b4662 --- /dev/null +++ b/src/ProxyInterfaceSourceGenerator/Utils/MemberHelper.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace ProxyInterfaceSourceGenerator.Utils +{ + internal static class MemberHelper + { + private static string[] ExcludedMethods = new string[] { "ToString", "GetHashCode" }; + + public static IEnumerable GetPublicProperties(INamedTypeSymbol classSymbol, Func filter) + { + return GetPublicMembers(classSymbol, p => p.Kind == SymbolKind.Property, filter); + } + + public static IEnumerable GetPublicMethods(INamedTypeSymbol classSymbol, Func filter = null) + { + if (filter == null) + { + filter = _ => true; + } + + return GetPublicMembers(classSymbol, + m => m.Kind == SymbolKind.Method, + m => m.MethodKind == MethodKind.Ordinary, + m => !ExcludedMethods.Contains(m.Name), + filter); + } + + private static IEnumerable GetPublicMembers(INamedTypeSymbol classSymbol, params Func[] filters) where T : ISymbol + { + var membersQuery = classSymbol.GetMembers().OfType() + .Where(m => m.DeclaredAccessibility == Accessibility.Public); + + foreach (var filter in filters) + { + membersQuery = membersQuery.Where(filter); + } + + var members = membersQuery.ToList(); + + var propertyNames = membersQuery.Select(x => x.Name); + + var baseType = classSymbol.BaseType; + + while (baseType != null) + { + var baseMembers = baseType.GetMembers().OfType() + .Where(m => m.DeclaredAccessibility == Accessibility.Public) + .Where(x => !propertyNames.Contains(x.Name)); + + foreach (var filter in filters) + { + baseMembers = baseMembers.Where(filter); + } + + members.AddRange(baseMembers); + + baseType = baseType.BaseType; + } + + return membersQuery; + } + } +} \ No newline at end of file