commit 8ad4811b7b1317bd7e3f1b475cac02e41ff8f1ad Author: daver32 <38791383+daver32@users.noreply.github.com> Date: Sat Apr 10 00:46:40 2021 +0200 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ce6fdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,340 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- Backup*.rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb \ No newline at end of file diff --git a/InterfaceGenerator.Tests/AccessorsGenerationTests.cs b/InterfaceGenerator.Tests/AccessorsGenerationTests.cs new file mode 100644 index 0000000..90267a4 --- /dev/null +++ b/InterfaceGenerator.Tests/AccessorsGenerationTests.cs @@ -0,0 +1,135 @@ +using FluentAssertions; +using FluentAssertions.Common; +using Xunit; + +namespace InterfaceGenerator.Tests +{ + public class AccessorsGenerationTests + { + private readonly IAccessorsTestsService _sut; + + public AccessorsGenerationTests() + { + _sut = new AccessorsTestsService(); + } + + [Fact] + public void GetSetIndexer_IsImplemented() + { + var indexer = typeof(IAccessorsTestsService).GetIndexerByParameterTypes(new[] { typeof(string) }); + + indexer.Should().NotBeNull(); + + indexer.GetMethod.Should().NotBeNull(); + indexer.SetMethod.Should().NotBeNull(); + + int _ = _sut[string.Empty]; + _sut[string.Empty] = 0; + } + + [Fact] + public void PublicProperty_IsImplemented() + { + var prop = typeof(IAccessorsTestsService) + .GetProperty(nameof(IAccessorsTestsService.PublicProperty)); + + prop.Should().NotBeNull(); + + prop.GetMethod.Should().NotBeNull(); + prop.SetMethod.Should().NotBeNull(); + + string _ = _sut.PublicProperty; + _sut.PublicProperty = string.Empty; + } + + [Fact] + public void PrivateSetter_IsOmitted() + { + var prop = typeof(IAccessorsTestsService) + .GetProperty(nameof(IAccessorsTestsService.PropertyWithPrivateSetter)); + + prop.Should().NotBeNull(); + + prop.GetMethod.Should().NotBeNull(); + prop.SetMethod.Should().BeNull(); + + string _ = _sut.PropertyWithPrivateSetter; + } + + [Fact] + public void PrivateGetter_IsOmitted() + { + var prop = typeof(IAccessorsTestsService) + .GetProperty(nameof(IAccessorsTestsService.PropertyWithPrivateGetter)); + + prop.Should().NotBeNull(); + + prop.SetMethod.Should().NotBeNull(); + prop.GetMethod.Should().BeNull(); + + _sut.PropertyWithPrivateGetter = string.Empty; + } + + [Fact] + public void ProtectedSetter_IsOmitted() + { + var prop = typeof(IAccessorsTestsService) + .GetProperty(nameof(IAccessorsTestsService.PropertyWithProtectedSetter)); + + prop.Should().NotBeNull(); + + prop.GetMethod.Should().NotBeNull(); + prop.SetMethod.Should().BeNull(); + + string _ = _sut.PropertyWithProtectedSetter; + } + + [Fact] + public void ProtectedGetter_IsOmitted() + { + var prop = typeof(IAccessorsTestsService) + .GetProperty(nameof(IAccessorsTestsService.PropertyWithProtectedGetter)); + + prop.Should().NotBeNull(); + + prop.SetMethod.Should().NotBeNull(); + prop.GetMethod.Should().BeNull(); + + _sut.PropertyWithProtectedGetter = string.Empty; + } + + [Fact] + public void IgnoredProperty_IsOmitted() + { + var prop = typeof(IAccessorsTestsService) + .GetProperty(nameof(AccessorsTestsService.IgnoredProperty)); + + prop.Should().BeNull(); + } + } + + // ReSharper disable UnusedMember.Local, ValueParameterNotUsed + [GenerateAutoInterface] + internal class AccessorsTestsService : IAccessorsTestsService + { + public int this[string x] + { + get => 0; + set { } + } + + public string PublicProperty { get; set; } + + public string PropertyWithPrivateSetter { get; private set; } + + public string PropertyWithPrivateGetter { private get; set; } + + public string PropertyWithProtectedSetter { get; protected set; } + + public string PropertyWithProtectedGetter { protected get; set; } + + [AutoInterfaceIgnore] + public string IgnoredProperty { get; set; } + } + // ReSharper enable UnusedMember.Local, ValueParameterNotUsed +} \ No newline at end of file diff --git a/InterfaceGenerator.Tests/GenericInterfaceTests.cs b/InterfaceGenerator.Tests/GenericInterfaceTests.cs new file mode 100644 index 0000000..fb5575c --- /dev/null +++ b/InterfaceGenerator.Tests/GenericInterfaceTests.cs @@ -0,0 +1,37 @@ +using System; +using System.Reflection; +using FluentAssertions; +using Xunit; + +namespace InterfaceGenerator.Tests +{ + public class GenericInterfaceTests + { + [Fact] + public void GenericParametersGeneratedCorrectly() + { + var genericArgs = typeof(IGenericInterfaceTestsService<,>).GetGenericArguments(); + + genericArgs.Should().HaveCount(2); + genericArgs[0].Name.Should().Be("TX"); + genericArgs[1].Name.Should().Be("TY"); + + genericArgs[0].IsClass.Should().BeTrue(); + genericArgs[0].GenericParameterAttributes + .Should().HaveFlag(GenericParameterAttributes.DefaultConstructorConstraint); + + var iEquatableOfTx = typeof(IEquatable<>).MakeGenericType(genericArgs[0]); + genericArgs[0].GetGenericParameterConstraints().Should().HaveCount(1).And.Contain(iEquatableOfTx); + + genericArgs[1].IsValueType.Should().BeTrue(); + } + } + + [GenerateAutoInterface] + // ReSharper disable once UnusedType.Global + internal class GenericInterfaceTestsService : IGenericInterfaceTestsService + where TX : class, IEquatable, new() + where TY : struct + { + } +} \ No newline at end of file diff --git a/InterfaceGenerator.Tests/InterfaceGenerator.Tests.csproj b/InterfaceGenerator.Tests/InterfaceGenerator.Tests.csproj new file mode 100644 index 0000000..912fbda --- /dev/null +++ b/InterfaceGenerator.Tests/InterfaceGenerator.Tests.csproj @@ -0,0 +1,20 @@ + + + + net5.0 + + false + + + + + + + + + + + + + + diff --git a/InterfaceGenerator.Tests/MethodGenerationTests.cs b/InterfaceGenerator.Tests/MethodGenerationTests.cs new file mode 100644 index 0000000..393f68f --- /dev/null +++ b/InterfaceGenerator.Tests/MethodGenerationTests.cs @@ -0,0 +1,305 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using FluentAssertions; +using Xunit; + +namespace InterfaceGenerator.Tests +{ + public class MethodGenerationTests + { + private readonly IMethodsTestService _sut; + + public MethodGenerationTests() + { + _sut = new MethodsTestService(); + } + + [Fact] + public void VoidMethod_IsImplemented() + { + var method = typeof(IMethodsTestService).GetMethod( + nameof(MethodsTestService.VoidMethod)); + + method.Should().NotBeNull(); + method.ReturnType.Should().Be(typeof(void)); + + var parameters = method.GetParameters(); + parameters.Should().BeEmpty(); + + _sut.VoidMethod(); + } + + [Fact] + public void VoidMethodWithParams_IsImplemented() + { + var method = typeof(IMethodsTestService).GetMethod( + nameof(MethodsTestService.VoidMethodWithParams), + new[] { typeof(string), typeof(string) }); + + method.Should().NotBeNull(); + method.ReturnType.Should().Be(typeof(void)); + + var parameters = method.GetParameters(); + parameters.Select(x => x.ParameterType).Should().AllBeEquivalentTo(typeof(string)); + parameters.Should().HaveCount(2); + + _sut.VoidMethodWithParams(string.Empty, string.Empty); + } + + [Fact] + public void VoidMethodWithOutParam_IsImplemented() + { + var method = typeof(IMethodsTestService).GetMethod( + nameof(MethodsTestService.VoidMethodWithOutParam), + new[] { typeof(string).MakeByRefType() }); + + method.Should().NotBeNull(); + method.ReturnType.Should().Be(typeof(void)); + + var parameters = method.GetParameters(); + parameters.Select(x => x.ParameterType).Should().AllBeEquivalentTo(typeof(string).MakeByRefType()); + parameters.Should().HaveCount(1); + parameters[0].IsOut.Should().BeTrue(); + + _sut.VoidMethodWithOutParam(out var _); + } + + [Fact] + public void VoidMethodWithInParam_IsImplemented() + { + var method = typeof(IMethodsTestService).GetMethod( + nameof(MethodsTestService.VoidMethodWithInParam), + new[] { typeof(string).MakeByRefType() }); + + method.Should().NotBeNull(); + method.ReturnType.Should().Be(typeof(void)); + + var parameters = method.GetParameters(); + parameters.Select(x => x.ParameterType).Should().AllBeEquivalentTo(typeof(string).MakeByRefType()); + parameters.Should().HaveCount(1); + parameters[0].IsIn.Should().BeTrue(); + + var stub = string.Empty; + _sut.VoidMethodWithInParam(in stub); + } + + [Fact] + public void VoidMethodWithRefParam_IsImplemented() + { + var method = typeof(IMethodsTestService).GetMethod( + nameof(MethodsTestService.VoidMethodWithRefParam), + new[] { typeof(string).MakeByRefType() }); + + method.Should().NotBeNull(); + method.ReturnType.Should().Be(typeof(void)); + + var parameters = method.GetParameters(); + parameters.Select(x => x.ParameterType).Should().AllBeEquivalentTo(typeof(string).MakeByRefType()); + parameters.Should().HaveCount(1); + parameters[0].IsIn.Should().BeFalse(); + parameters[0].IsOut.Should().BeFalse(); + + var stub = string.Empty; + _sut.VoidMethodWithRefParam(ref stub); + } + + [Fact] + public void StringMethod_IsImplemented() + { + var method = typeof(IMethodsTestService).GetMethod( + nameof(MethodsTestService.StringMethod)); + + method.Should().NotBeNull(); + method.ReturnType.Should().Be(typeof(string)); + + var parameters = method.GetParameters(); + parameters.Should().BeEmpty(); + + var _ = _sut.StringMethod(); + } + + [Fact] + public void GenericVoidMethod_IsImplemented() + { + var method = typeof(IMethodsTestService) + .GetMethods() + .FirstOrDefault(x => x.Name == nameof(MethodsTestService.GenericVoidMethod)); + + method.Should().NotBeNull(); + method.ReturnType.Should().Be(typeof(void)); + + method.GetParameters().Should().BeEmpty(); + + var genericArgs = method.GetGenericArguments(); + genericArgs.Should().HaveCount(2); + + _sut.GenericVoidMethod(); + } + + [Fact] + public void GenericVoidMethodWithGenericParam_IsImplemented() + { + var method = typeof(IMethodsTestService) + .GetMethods() + .FirstOrDefault(x => x.Name == nameof(MethodsTestService.GenericVoidMethodWithGenericParam)); + + method.Should().NotBeNull(); + method.ReturnType.Should().Be(typeof(void)); + + var genericArgs = method.GetGenericArguments(); + genericArgs.Should().HaveCount(2); + + var parameters = method.GetParameters(); + parameters.Should().HaveCount(1); + parameters[0].ParameterType.Should().Be(genericArgs[0]); + + _sut.GenericVoidMethodWithGenericParam(string.Empty); + } + + [Fact] + public void GenericVoidMethodWithConstraints_IsImplemented() + { + var method = typeof(IMethodsTestService) + .GetMethods() + .FirstOrDefault(x => x.Name == nameof(MethodsTestService.GenericVoidMethodWithConstraints)); + + method.Should().NotBeNull(); + method.ReturnType.Should().Be(typeof(void)); + + var genericArgs = method.GetGenericArguments(); + genericArgs.Should().HaveCount(2); + + genericArgs[0].IsClass.Should().BeTrue(); + genericArgs[0].GetGenericParameterConstraints().Should().HaveCount(0); + + genericArgs[1].IsClass.Should().BeTrue(); + genericArgs[1].GetGenericParameterConstraints().Should().HaveCount(1); + genericArgs[1].GetGenericParameterConstraints()[0].Should().Be(genericArgs[0]); + genericArgs[1].GenericParameterAttributes.Should() + .HaveFlag(GenericParameterAttributes.DefaultConstructorConstraint); + + _sut.GenericVoidMethodWithConstraints(); + } + + [Fact] + public void VoidMethodWithOptionalParams_IsImplemented() + { + var method = typeof(IMethodsTestService) + .GetMethods() + .FirstOrDefault(x => x.Name == nameof(MethodsTestService.VoidMethodWithOptionalParams)); + + method.Should().NotBeNull(); + method.ReturnType.Should().Be(typeof(void)); + + var parameters = method.GetParameters(); + parameters.Should().HaveCount(5); + parameters.Select(x => x.IsOptional).Should().AllBeEquivalentTo(true); + + parameters[0].DefaultValue.Should().Be("cGFyYW0="); + parameters[1].DefaultValue.Should().Be(MethodsTestService.StringConstant); + parameters[2].DefaultValue.Should().Be(0.1f); + parameters[3].DefaultValue.Should().Be(0.2d); + parameters[4].DefaultValue.Should().Be(0.3d); + + _sut.VoidMethodWithOptionalParams(); + } + + [Fact] + public void VoidMethodWithExpandingParam_IsImplemented() + { + var method = typeof(IMethodsTestService) + .GetMethods() + .FirstOrDefault(x => x.Name == nameof(MethodsTestService.VoidMethodWithExpandingParam)); + + method.Should().NotBeNull(); + method.ReturnType.Should().Be(typeof(void)); + + var parameters = method.GetParameters(); + parameters.Should().HaveCount(1); + parameters[0].ParameterType.Should().Be(typeof(string[])); + parameters[0].GetCustomAttribute().Should().NotBeNull(); + } + + [Fact] + public void IgnoreMethod_IsOmitted() + { + var method = typeof(IMethodsTestService) + .GetMethods() + .FirstOrDefault(x => x.Name == nameof(MethodsTestService.IgnoredMethod)); + + method.Should().BeNull(); + } + } + + [GenerateAutoInterface] + internal class MethodsTestService : IMethodsTestService + { + public const string StringConstant = "Const"; + + public void VoidMethod() + { + } + + public void VoidMethodWithParams(string a, string b) + { + } + + public void VoidMethodWithOutParam(out string a) + { + a = default; + } + + public void VoidMethodWithRefParam(ref string a) + { + } + + public void VoidMethodWithInParam(in string a) + { + } + + public string StringMethod() + { + return string.Empty; + } + + public void GenericVoidMethod() + { + } + + public void GenericVoidMethodWithGenericParam(TX a) + { + } + + public void GenericVoidMethodWithConstraints() + where TX : class + where TY : class, TX, new() + { + } + + public void VoidMethodWithOptionalParams( + string stringLiteral = "cGFyYW0=", + string stringConstant = StringConstant, + float floatLiteral = 0.1f, + double doubleLiteral = 0.2, + decimal decimalLiteral = 0.3m) + { + } + + public void VoidMethodWithExpandingParam(params string[] strings) + { + } + + [AutoInterfaceIgnore] + public void IgnoredMethod() + { + + } + } + + [GenerateAutoInterface] + internal class MethodsTestServiceGeneric : IMethodsTestServiceGeneric where T : class + { + } +} \ No newline at end of file diff --git a/InterfaceGenerator.Tests/VisibilityModifierTests.cs b/InterfaceGenerator.Tests/VisibilityModifierTests.cs new file mode 100644 index 0000000..5751fd9 --- /dev/null +++ b/InterfaceGenerator.Tests/VisibilityModifierTests.cs @@ -0,0 +1,57 @@ +using System.Reflection; +using FluentAssertions; +using Xunit; + +namespace InterfaceGenerator.Tests +{ + public class VisibilityModifierTests + { + [Fact] + public void IExplicitlyPublicService_IsPublic() + { + var type = typeof(IExplicitlyPublicService); + type.Attributes.Should().HaveFlag(TypeAttributes.Public); + } + + [Fact] + public void IExplicitlyInternalService_IsInternal() + { + var type = typeof(IExplicitlyInternalService); + type.Attributes.Should().HaveFlag(TypeAttributes.NotPublic); + } + + [Fact] + public void IImplicitlyPublicService_IsPublic() + { + var type = typeof(IImplicitlyPublicService); + type.Attributes.Should().HaveFlag(TypeAttributes.Public); + } + + [Fact] + public void IImplicitlyInternalService_IsInternal() + { + var type = typeof(IImplicitlyInternalService); + type.Attributes.Should().HaveFlag(TypeAttributes.NotPublic); + } + } + + [GenerateAutoInterface(VisibilityModifier = "public")] + internal class ExplicitlyPublicService : IExplicitlyPublicService + { + } + + [GenerateAutoInterface(VisibilityModifier = "internal")] + public class ExplicitlyInternalService : IExplicitlyInternalService + { + } + + [GenerateAutoInterface] + public class ImplicitlyPublicService : IImplicitlyPublicService + { + } + + [GenerateAutoInterface] + internal class ImplicitlyInternalService : IImplicitlyInternalService + { + } +} \ No newline at end of file diff --git a/InterfaceGenerator.sln b/InterfaceGenerator.sln new file mode 100644 index 0000000..3cae7ca --- /dev/null +++ b/InterfaceGenerator.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceGenerator", "InterfaceGenerator\InterfaceGenerator.csproj", "{D40E2D60-5580-42D2-8316-F5AAA42CFBF6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceGenerator.Tests", "InterfaceGenerator.Tests\InterfaceGenerator.Tests.csproj", "{D39C541E-9EDC-41E9-BBD8-FAA9DA602CC0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D40E2D60-5580-42D2-8316-F5AAA42CFBF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D40E2D60-5580-42D2-8316-F5AAA42CFBF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D40E2D60-5580-42D2-8316-F5AAA42CFBF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D40E2D60-5580-42D2-8316-F5AAA42CFBF6}.Release|Any CPU.Build.0 = Release|Any CPU + {D39C541E-9EDC-41E9-BBD8-FAA9DA602CC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D39C541E-9EDC-41E9-BBD8-FAA9DA602CC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D39C541E-9EDC-41E9-BBD8-FAA9DA602CC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D39C541E-9EDC-41E9-BBD8-FAA9DA602CC0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/InterfaceGenerator/.gitattributes b/InterfaceGenerator/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/InterfaceGenerator/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/InterfaceGenerator/Attributes.cs b/InterfaceGenerator/Attributes.cs new file mode 100644 index 0000000..4d1c808 --- /dev/null +++ b/InterfaceGenerator/Attributes.cs @@ -0,0 +1,45 @@ +using System; + +namespace InterfaceGenerator +{ + + internal class Attributes + { + public const string AttributesNamespace = nameof(InterfaceGenerator); + + public const string GenerateAutoInterfaceClassname = "GenerateAutoInterfaceAttribute"; + public const string AutoInterfaceIgnoreAttributeClassname = "AutoInterfaceIgnoreAttribute"; + + public const string VisibilityModifierPropName = "VisibilityModifier"; + public const string InterfaceNamePropName = "Name"; + + public static readonly string AttributesSourceCode = $@" + +using System; +using System.Diagnostics; + +#nullable enable + +namespace {AttributesNamespace} +{{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] + [Conditional(""CodeGeneration"")] + public sealed class {GenerateAutoInterfaceClassname} : Attribute + {{ + public string? {VisibilityModifierPropName} {{ get; init; }} + public string? {InterfaceNamePropName} {{ get; init; }} + + public {GenerateAutoInterfaceClassname}() + {{ + }} + }} + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false)] + [Conditional(""CodeGeneration"")] + public sealed class {AutoInterfaceIgnoreAttributeClassname} : Attribute + {{ + }} +}} +"; + } +} \ No newline at end of file diff --git a/InterfaceGenerator/AutoInterfaceGenerator.cs b/InterfaceGenerator/AutoInterfaceGenerator.cs new file mode 100644 index 0000000..9709410 --- /dev/null +++ b/InterfaceGenerator/AutoInterfaceGenerator.cs @@ -0,0 +1,395 @@ +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace InterfaceGenerator +{ + [Generator] + public class AutoInterfaceGenerator : ISourceGenerator + { + private INamedTypeSymbol _generateAutoInterfaceAttribute = null!; + private INamedTypeSymbol _ignoreAttribute = null!; + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + // setting the culture to invariant prevents errors such as emitting a decimal comma (0,1) instead of + // a decimal point (0.1) in certain cultures + var prevCulture = Thread.CurrentThread.CurrentCulture; + Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; + + GenerateAttributes(context); + GenerateInterfaces(context); + + Thread.CurrentThread.CurrentCulture = prevCulture; + } + + private static void GenerateAttributes(GeneratorExecutionContext context) + { + context.AddSource( + Attributes.GenerateAutoInterfaceClassname, + SourceText.From(Attributes.AttributesSourceCode, Encoding.UTF8)); + } + + private void GenerateInterfaces(GeneratorExecutionContext context) + { + if (context.SyntaxReceiver is not SyntaxReceiver receiver) + return; + + var compilation = GetCompilation(context); + InitAttributes(compilation); + + var classSymbols = GetImplTypeSymbols(compilation, receiver); + + foreach (var implTypeSymbol in classSymbols) + { + if (!implTypeSymbol.TryGetAttribute(_generateAutoInterfaceAttribute, out var attributes)) + { + continue; + } + + var attribute = attributes.Single(); + var source = SourceText.From(GenerateInterfaceCode(implTypeSymbol, attribute), Encoding.UTF8); + context.AddSource($"{implTypeSymbol.Name}_AutoInterface.cs", source); + } + } + + private string GetVisibilityModifier(INamedTypeSymbol implTypeSymbol, AttributeData attributeData) + { + var pair = attributeData.NamedArguments.FirstOrDefault(x => x.Key == Attributes.VisibilityModifierPropName); + string? result = pair.Value.Value?.ToString(); + + if (!string.IsNullOrEmpty(result)) + { + return result; + } + + return implTypeSymbol.DeclaredAccessibility switch + { + Accessibility.Public => "public", + var _ => "internal", + }; + } + + private string GetInterfaceName(INamedTypeSymbol implTypeSymbol, AttributeData attributeData) + { + var pair = attributeData.NamedArguments.FirstOrDefault(x => x.Key == Attributes.InterfaceNamePropName); + string? result = pair.Value.Value?.ToString(); + + if (!string.IsNullOrEmpty(result)) + { + return result; + } + + return "I" + implTypeSymbol.Name; + } + + private string GenerateInterfaceCode(INamedTypeSymbol implTypeSymbol, AttributeData attributeData) + { + using var stream = new MemoryStream(); + var streamWriter = new StreamWriter(stream, Encoding.UTF8); + var codeWriter = new IndentedTextWriter(streamWriter, GeneratorConsts.Indent); + + var namespaceName = implTypeSymbol.ContainingNamespace.ToDisplayString(); + var interfaceName = GetInterfaceName(implTypeSymbol, attributeData); + var visibilityModifier = GetVisibilityModifier(implTypeSymbol, attributeData); + + codeWriter.WriteLine("namespace {0}", namespaceName); + codeWriter.WriteLine("{"); + + ++codeWriter.Indent; + codeWriter.Write("{0} partial interface {1}", visibilityModifier, interfaceName); + WriteTypeGenericsIfNeeded(codeWriter, implTypeSymbol); + codeWriter.WriteLine(); + codeWriter.WriteLine("{"); + + ++codeWriter.Indent; + GenerateInterfaceMemberDefinitions(codeWriter, implTypeSymbol); + --codeWriter.Indent; + + codeWriter.WriteLine("}"); + --codeWriter.Indent; + + codeWriter.WriteLine("}"); + + codeWriter.Flush(); + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream, Encoding.UTF8, true); + return reader.ReadToEnd(); + } + + private void WriteTypeGenericsIfNeeded(IndentedTextWriter writer, INamedTypeSymbol implTypeSymbol) + { + if (!implTypeSymbol.IsGenericType) + { + return; + } + + writer.Write("<"); + writer.WriteJoin(", ", implTypeSymbol.TypeParameters.Select(x => x.Name)); + writer.Write(">"); + + WriteTypeParameterConstraints(writer, implTypeSymbol.TypeParameters); + } + + private void GenerateInterfaceMemberDefinitions(IndentedTextWriter writer, INamedTypeSymbol implTypeSymbol) + { + foreach (var member in implTypeSymbol.GetMembers()) + { + if (member.DeclaredAccessibility != Accessibility.Public) + { + continue; + } + + if (member.HasAttribute(_ignoreAttribute)) + { + continue; + } + + GenerateInterfaceMemberDefinition(writer, member); + } + } + + private static void GenerateInterfaceMemberDefinition(IndentedTextWriter writer, ISymbol member) + { + switch (member) + { + case IPropertySymbol propertySymbol: + GeneratePropertyDefinition(writer, propertySymbol); + break; + case IMethodSymbol methodSymbol: + GenerateMethodDefinition(writer, methodSymbol); + break; + } + } + + private static void WriteMemberDocs(IndentedTextWriter writer, ISymbol member) + { + var xml = member.GetDocumentationCommentXml(); + if (string.IsNullOrWhiteSpace(xml)) + { + return; + } + + // omit the fist and last lines to skip the tag + + var reader = new StringReader(xml); + var lines = new List(); + + while (true) + { + var line = reader.ReadLine(); + if (line is null) + { + break; + } + + lines.Add(line); + } + + for (int i = 1; i < lines.Count - 1; i++) + { + var line = lines[i]; + writer.WriteLine("/// {0}", line); + } + } + + private static bool IsPublicOrInternal(IMethodSymbol methodSymbol) + { + return methodSymbol.DeclaredAccessibility == Accessibility.Public || + methodSymbol.DeclaredAccessibility == Accessibility.Internal; + } + + private static void GeneratePropertyDefinition(IndentedTextWriter writer, IPropertySymbol propertySymbol) + { + bool hasPublicGetter = propertySymbol.GetMethod is not null && + IsPublicOrInternal(propertySymbol.GetMethod); + + bool hasPublicSetter = propertySymbol.SetMethod is not null && + IsPublicOrInternal(propertySymbol.SetMethod); + + if (!hasPublicGetter && !hasPublicSetter) + { + return; + } + + WriteMemberDocs(writer, propertySymbol); + + if (propertySymbol.IsIndexer) + { + writer.Write("{0} this[", propertySymbol.Type); + //writer.WriteJoin(", ", propertySymbol.Parameters, (w, param) => { w.Write("{0} {1}", param.Type, param.Name); }); + writer.WriteJoin(", ", propertySymbol.Parameters, WriteMethodParam); + writer.Write("] "); + } + else + { + writer.Write("{0} {1} ", propertySymbol.Type, propertySymbol.Name); // ex. int Foo + } + + writer.Write("{ "); + + if (hasPublicGetter) + { + writer.Write("get; "); + } + + if (hasPublicSetter) + { + writer.Write("set; "); + } + + writer.WriteLine("}"); + } + + private static void GenerateMethodDefinition(IndentedTextWriter writer, IMethodSymbol methodSymbol) + { + if (methodSymbol.MethodKind != MethodKind.Ordinary || methodSymbol.IsStatic) + { + return; + } + + WriteMemberDocs(writer, methodSymbol); + + writer.Write("{0} {1}", methodSymbol.ReturnType, methodSymbol.Name); // ex. int Foo + + if (methodSymbol.IsGenericMethod) + { + writer.Write("<"); + writer.WriteJoin(", ", methodSymbol.TypeParameters.Select(x => x.Name)); + writer.Write(">"); + } + + writer.Write("("); + writer.WriteJoin(", ", methodSymbol.Parameters, WriteMethodParam); + + writer.Write(")"); + + if (methodSymbol.IsGenericMethod) + { + WriteTypeParameterConstraints(writer, methodSymbol.TypeParameters); + } + + writer.WriteLine(";"); + } + + private static void WriteMethodParam(TextWriter writer, IParameterSymbol param) + { + if (param.IsParams) + { + writer.Write("params "); + } + + switch (param.RefKind) + { + case RefKind.Ref: + writer.Write("ref "); + break; + case RefKind.Out: + writer.Write("out "); + break; + case RefKind.In: + writer.Write("in "); + break; + } + + writer.Write("{0} {1}", param.Type, param.Name); + + if (param.HasExplicitDefaultValue) + { + WriteParamExplicitDefaultValue(writer, param); + } + } + + private static void WriteParamExplicitDefaultValue(TextWriter writer, IParameterSymbol param) + { + if (param.ExplicitDefaultValue is null) + { + writer.Write(" = default"); + } + else + { + switch (param.Type.Name) + { + case nameof(String): + writer.Write(" = \"{0}\"", param.ExplicitDefaultValue); + break; + case nameof(Single): + writer.Write(" = {0}f", param.ExplicitDefaultValue); + break; + case nameof(Double): + writer.Write(" = {0}d", param.ExplicitDefaultValue); + break; + case nameof(Decimal): + writer.Write(" = {0}m", param.ExplicitDefaultValue); + break; + default: + writer.Write(" = {0}", param.ExplicitDefaultValue); + break; + } + } + } + + private static void WriteTypeParameterConstraints( + IndentedTextWriter writer, + IEnumerable typeParameters) + { + foreach (var typeParameter in typeParameters) + { + var constraints = typeParameter.EnumGenericConstraints().ToList(); + if (constraints.Count == 0) + { + break; + } + + writer.Write(" where {0} : ", typeParameter.Name); + writer.WriteJoin(", ", constraints); + } + } + + private void InitAttributes(Compilation compilation) + { + _generateAutoInterfaceAttribute = compilation.GetTypeByMetadataName( + $"{Attributes.AttributesNamespace}.{Attributes.GenerateAutoInterfaceClassname}")!; + + _ignoreAttribute = compilation.GetTypeByMetadataName( + $"{Attributes.AttributesNamespace}.{Attributes.AutoInterfaceIgnoreAttributeClassname}")!; + } + + private static IEnumerable GetImplTypeSymbols(Compilation compilation, SyntaxReceiver receiver) + { + return receiver.CandidateClasses.Select(candidate => GetClassSymbol(compilation, candidate)); + } + + private static INamedTypeSymbol GetClassSymbol(Compilation compilation, ClassDeclarationSyntax @class) + { + var model = compilation.GetSemanticModel(@class.SyntaxTree); + var classSymbol = ModelExtensions.GetDeclaredSymbol(model, @class)!; + return (INamedTypeSymbol)classSymbol; + } + + private static Compilation GetCompilation(GeneratorExecutionContext context) + { + var options = context.Compilation.SyntaxTrees.First().Options as CSharpParseOptions; + + var compilation = context.Compilation.AddSyntaxTrees( + CSharpSyntaxTree.ParseText( + SourceText.From(Attributes.AttributesSourceCode, Encoding.UTF8), options)); + + return compilation; + } + } +} \ No newline at end of file diff --git a/InterfaceGenerator/GeneratorConsts.cs b/InterfaceGenerator/GeneratorConsts.cs new file mode 100644 index 0000000..a4b56f3 --- /dev/null +++ b/InterfaceGenerator/GeneratorConsts.cs @@ -0,0 +1,7 @@ +namespace InterfaceGenerator +{ + internal class GeneratorConsts + { + public const string Indent = " "; + } +} \ No newline at end of file diff --git a/InterfaceGenerator/InterfaceGenerator.csproj b/InterfaceGenerator/InterfaceGenerator.csproj new file mode 100644 index 0000000..fcf1da0 --- /dev/null +++ b/InterfaceGenerator/InterfaceGenerator.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.1 + 9.0 + enable + R. David + + + + + + + diff --git a/InterfaceGenerator/SymbolExtensions.cs b/InterfaceGenerator/SymbolExtensions.cs new file mode 100644 index 0000000..bb1fbce --- /dev/null +++ b/InterfaceGenerator/SymbolExtensions.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace InterfaceGenerator +{ + internal static class SymbolExtensions + { + public static bool TryGetAttribute( + this ISymbol symbol, + INamedTypeSymbol attributeType, + out IEnumerable attributes) + { + attributes = symbol.GetAttributes() + .Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); + return attributes.Any(); + } + + public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeType) + { + return symbol.GetAttributes() + .Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); + } + } +} \ No newline at end of file diff --git a/InterfaceGenerator/SyntaxReceiver.cs b/InterfaceGenerator/SyntaxReceiver.cs new file mode 100644 index 0000000..7ca7f04 --- /dev/null +++ b/InterfaceGenerator/SyntaxReceiver.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace InterfaceGenerator +{ + internal class SyntaxReceiver : ISyntaxReceiver + { + public IList CandidateClasses { get; } = new List(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax && + classDeclarationSyntax.AttributeLists.Count > 0) + { + CandidateClasses.Add(classDeclarationSyntax); + } + } + } +} \ No newline at end of file diff --git a/InterfaceGenerator/TextWriterExtensions.cs b/InterfaceGenerator/TextWriterExtensions.cs new file mode 100644 index 0000000..94092e9 --- /dev/null +++ b/InterfaceGenerator/TextWriterExtensions.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace InterfaceGenerator +{ + internal static class TextWriterExtensions + { + + public static void WriteJoin( + this TextWriter writer, + string separator, + IEnumerable values) + { + writer.WriteJoin(separator, values, (w, x) => w.Write(x)); + } + + public static void WriteJoin( + this TextWriter writer, + string separator, + IEnumerable values, + Action writeAction) + { + string.Join("", Enumerable.Empty()); + + using var enumerator = values.GetEnumerator(); + + if (!enumerator.MoveNext()) + { + return; + } + + writeAction(writer, enumerator.Current); + + if (!enumerator.MoveNext()) + { + return; + } + + do + { + writer.Write(separator); + writeAction(writer, enumerator.Current); + } while (enumerator.MoveNext()); + } + } +} \ No newline at end of file diff --git a/InterfaceGenerator/TypeParameterSymbolExtensions.cs b/InterfaceGenerator/TypeParameterSymbolExtensions.cs new file mode 100644 index 0000000..bfaa883 --- /dev/null +++ b/InterfaceGenerator/TypeParameterSymbolExtensions.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace InterfaceGenerator +{ + internal static class TypeParameterSymbolExtensions + { + public static IEnumerable EnumGenericConstraints(this ITypeParameterSymbol symbol) + { + // the class/struct/unmanaged/notnull constraint has to be the last + if (symbol.HasNotNullConstraint) + { + yield return "notnull"; + } + + if (symbol.HasValueTypeConstraint) + { + yield return "struct"; + } + + if (symbol.HasUnmanagedTypeConstraint) + { + yield return "unmanaged"; + } + + if (symbol.HasReferenceTypeConstraint) + { + yield return symbol.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated + ? "class?" + : "class"; + } + + + // types go in the middle + foreach (var constraintType in symbol.ConstraintTypes) + { + yield return constraintType.ToDisplayString(); + } + + + // the new() constraint has to be the last + if (symbol.HasConstructorConstraint) + { + yield return "new()"; + } + } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c449bd4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 daver32 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file