diff --git a/InterfaceGenerator.Contract/AutoInterfaceIgnoreAttribute.cs b/InterfaceGenerator.Contract/AutoInterfaceIgnoreAttribute.cs new file mode 100644 index 0000000..8566592 --- /dev/null +++ b/InterfaceGenerator.Contract/AutoInterfaceIgnoreAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace InterfaceGenerator +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = true)] + public class AutoInterfaceIgnoreAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/InterfaceGenerator.Contract/AutoInterfaceNameTemplateAttribute.cs b/InterfaceGenerator.Contract/AutoInterfaceNameTemplateAttribute.cs new file mode 100644 index 0000000..625b243 --- /dev/null +++ b/InterfaceGenerator.Contract/AutoInterfaceNameTemplateAttribute.cs @@ -0,0 +1,23 @@ +using System; + +namespace InterfaceGenerator +{ + /// + /// Mark the attribute derived from GenerateAutoInterfaceAttribute or from GenerateGenericAutoInterfaceAttribute + /// with the attribute to override interface name template. + /// + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + public sealed class AutoInterfaceNameTemplateAttribute : Attribute + { + /// Default is "I{Name}" + public AutoInterfaceNameTemplateAttribute(string NameTemplate) + { + this.NameTemplate = NameTemplate; + } + + /// + /// Default is "I{Name}" + /// + public string NameTemplate { get; set; } = "I{Name}"; + } +} \ No newline at end of file diff --git a/InterfaceGenerator.Contract/GenerateAutoInterfaceAttribute.cs b/InterfaceGenerator.Contract/GenerateAutoInterfaceAttribute.cs new file mode 100644 index 0000000..0682713 --- /dev/null +++ b/InterfaceGenerator.Contract/GenerateAutoInterfaceAttribute.cs @@ -0,0 +1,23 @@ +using System; + +namespace InterfaceGenerator +{ + /// + /// Mark the class/struct with the attribute to generate auto interface. + /// If an implementation is public or Visibility modifier is public then auto interface will derive from IAutoInterface. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = true)] + public class GenerateAutoInterfaceAttribute : Attribute + { + public string? VisibilityModifier { get; set; } + public string? Name { get; set; } + /// + /// Default is "I{Name}" + /// + public string NameTemplate { get; set; } = "I{Name}"; + + public GenerateAutoInterfaceAttribute() + { + } + } +} \ No newline at end of file diff --git a/InterfaceGenerator.Contract/GenerateGenericAutoInterfaceAttribute.cs b/InterfaceGenerator.Contract/GenerateGenericAutoInterfaceAttribute.cs new file mode 100644 index 0000000..d9fb3c6 --- /dev/null +++ b/InterfaceGenerator.Contract/GenerateGenericAutoInterfaceAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace InterfaceGenerator +{ + /// + /// Mark the class/struct with the attribute to generate auto interface. + /// If an implementation is public or Visibility modifier is public then auto interface will derive from IAutoInterface. + /// TImplementer must be public. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = true)] + public class GenerateGenericAutoInterfaceAttribute : Attribute + { + public string? VisibilityModifier { get; set; } + public string? Name { get; set; } + /// + /// Default is "I{Name}" + /// + public string NameTemplate { get; set; } = "I{Name}"; + + public GenerateGenericAutoInterfaceAttribute() + { + } + } +} \ No newline at end of file diff --git a/InterfaceGenerator.Contract/IAutoInterface.cs b/InterfaceGenerator.Contract/IAutoInterface.cs new file mode 100644 index 0000000..c5a81ea --- /dev/null +++ b/InterfaceGenerator.Contract/IAutoInterface.cs @@ -0,0 +1,16 @@ +namespace InterfaceGenerator +{ + /// + /// The base interface for generated public auto interfaces + /// + public interface IAutoInterface + { + } + + /// + /// The base generic interface for generated public auto interfaces, where T is an implementation type + /// + public interface IAutoInterface : IAutoInterface + { + } +} \ No newline at end of file diff --git a/InterfaceGenerator.Contract/InterfaceGenerator.Contract.csproj b/InterfaceGenerator.Contract/InterfaceGenerator.Contract.csproj new file mode 100644 index 0000000..9d4ce57 --- /dev/null +++ b/InterfaceGenerator.Contract/InterfaceGenerator.Contract.csproj @@ -0,0 +1,9 @@ + + + + netstandard2 + latest + enable + + + diff --git a/InterfaceGenerator.Tests/GenericAutoInterfaceTests.cs b/InterfaceGenerator.Tests/GenericAutoInterfaceTests.cs new file mode 100644 index 0000000..23ad569 --- /dev/null +++ b/InterfaceGenerator.Tests/GenericAutoInterfaceTests.cs @@ -0,0 +1,49 @@ +using System; +using System.Reflection; +using FluentAssertions; +using Xunit; + +namespace InterfaceGenerator.Tests; + +public class GenericAutoInterfaceTests +{ + [Fact] + public void GenericParametersGeneratedCorrectly() + { + var t = typeof(IGenericAutoInterfaceTestsService<,>); + var genericArgs = t.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(); + + // base + var interfaces = t.GetInterfaces(); + interfaces.Should().HaveCount(2); + interfaces[0].Name.Should().Be(typeof(IAutoInterface<>).Name); + interfaces[1].Name.Should().Be(typeof(IAutoInterface).Name); + var interfaceGenericArgs = interfaces[0].GetGenericArguments(); + interfaceGenericArgs.Should().HaveCount(1); + interfaceGenericArgs[0].Name.Should().Be(typeof(GenericAutoInterfaceTestsService<,>).Name); + } +} + +[GenerateGenericAutoInterface] +// ReSharper disable once UnusedType.Global +public class GenericAutoInterfaceTestsService : IGenericAutoInterfaceTestsService + where TX : class, IEquatable, new () + where TY : struct +{ + public string A { get; set; } +} \ No newline at end of file diff --git a/InterfaceGenerator.Tests/GenericInterfaceTests.cs b/InterfaceGenerator.Tests/GenericInterfaceTests.cs index 8642e3b..9b919f7 100644 --- a/InterfaceGenerator.Tests/GenericInterfaceTests.cs +++ b/InterfaceGenerator.Tests/GenericInterfaceTests.cs @@ -10,7 +10,8 @@ public class GenericInterfaceTests [Fact] public void GenericParametersGeneratedCorrectly() { - var genericArgs = typeof(IGenericInterfaceTestsService<,>).GetGenericArguments(); + var t = typeof(IGenericInterfaceTestsService<,>); + var genericArgs = t.GetGenericArguments(); genericArgs.Should().HaveCount(2); genericArgs[0].Name.Should().Be("TX"); @@ -26,6 +27,19 @@ public void GenericParametersGeneratedCorrectly() genericArgs[0].GetGenericParameterConstraints().Should().HaveCount(1).And.Contain(iEquatableOfTx); genericArgs[1].IsValueType.Should().BeTrue(); + + // does not implement IAutoInterface because not public + t.GetInterfaces().Should().HaveCount(0); + } + + [Fact] + public void ImplementsIAutoInterface() + { + var t = typeof(IGenericInterfaceTestsService2<,>); + + var interfaces = t.GetInterfaces(); + interfaces.Should().HaveCount(1); + interfaces[0].Should().Be(typeof(IAutoInterface)); } } @@ -35,4 +49,12 @@ internal class GenericInterfaceTestsService : IGenericInterfaceTestsServ where TX : class, IEquatable, new() where TY : struct { +} + +[GenerateAutoInterface] +// ReSharper disable once UnusedType.Global +public class GenericInterfaceTestsService2 : IGenericInterfaceTestsService2 + 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 index db3e2fe..4db54ca 100644 --- a/InterfaceGenerator.Tests/InterfaceGenerator.Tests.csproj +++ b/InterfaceGenerator.Tests/InterfaceGenerator.Tests.csproj @@ -1,20 +1,30 @@ - - net6.0 + + net6.0 - false - + false + - - - - - - + + + + + + - - - + + + + + + + + + diff --git a/InterfaceGenerator.Tests/InterfaceNameTests.cs b/InterfaceGenerator.Tests/InterfaceNameTests.cs new file mode 100644 index 0000000..c8f50f1 --- /dev/null +++ b/InterfaceGenerator.Tests/InterfaceNameTests.cs @@ -0,0 +1,34 @@ +namespace InterfaceGenerator.Tests; + +[GenerateAutoInterface(Name = "ICustomNameInterface")] +internal class CustomName1 : ICustomNameInterface +{ + +} + +[GenerateAutoInterface(NameTemplate = "IPrefix{Name}Suffix")] +internal class CustomName2 : IPrefixCustomName2Suffix +{ + +} + +[NestedCustomName3(NameTemplate = "INested{Name}")] +internal class CustomName3 : INestedCustomName3 +{ + +} + +public class NestedCustomName3Attribute : GenerateAutoInterfaceAttribute +{ +} + +[NestedCustomName4] +public class CustomName4 : INestedCustomName4 +{ + +} + +[AutoInterfaceNameTemplate("INested{Name}")] +public class NestedCustomName4Attribute : GenerateGenericAutoInterfaceAttribute +{ +} \ No newline at end of file diff --git a/InterfaceGenerator.Tests/NestedAttributesTests.cs b/InterfaceGenerator.Tests/NestedAttributesTests.cs new file mode 100644 index 0000000..cf89d89 --- /dev/null +++ b/InterfaceGenerator.Tests/NestedAttributesTests.cs @@ -0,0 +1,181 @@ +using System.Runtime.CompilerServices; +using FluentAssertions; +using FluentAssertions.Common; +using Xunit; + +namespace InterfaceGenerator.Tests; + +public class NestedAttributesTests +{ + private readonly INestedAttributesService _sut; + + public NestedAttributesTests() + { + _sut = new NestedAttributesService(); + } + + [Fact] + public void GetSetIndexer_IsImplemented() + { + var indexer = typeof(INestedAttributesService).GetIndexerByParameterTypes(new[] { typeof(string) }); + + indexer.Should().NotBeNull(); + + indexer.GetMethod.Should().NotBeNull(); + indexer.SetMethod.Should().NotBeNull(); + + var _ = _sut[string.Empty]; + _sut[string.Empty] = 0; + } + + [Fact] + public void PublicProperty_IsImplemented() + { + var prop = typeof(INestedAttributesService) + .GetProperty(nameof(INestedAttributesService.PublicProperty))!; + + prop.Should().NotBeNull(); + + prop.GetMethod.Should().NotBeNull(); + prop.SetMethod.Should().NotBeNull(); + + var _ = _sut.PublicProperty; + _sut.PublicProperty = string.Empty; + } + + [Fact] + public void InitProperty_IsImplemented() + { + var prop = typeof(INestedAttributesService) + .GetProperty(nameof(INestedAttributesService.InitOnlyProperty))!; + + prop.Should().NotBeNull(); + + prop.GetMethod.Should().NotBeNull(); + prop.SetMethod.Should().NotBeNull(); + + prop.SetMethod!.ReturnParameter!.GetRequiredCustomModifiers().Should().Contain(typeof(IsExternalInit)); + + var _ = _sut.InitOnlyProperty; + } + + [Fact] + public void PrivateSetter_IsOmitted() + { + var prop = typeof(INestedAttributesService) + .GetProperty(nameof(INestedAttributesService.PropertyWithPrivateSetter))!; + + prop.Should().NotBeNull(); + + prop.GetMethod.Should().NotBeNull(); + prop.SetMethod.Should().BeNull(); + + var _ = _sut.PropertyWithPrivateSetter; + } + + [Fact] + public void PrivateGetter_IsOmitted() + { + var prop = typeof(INestedAttributesService) + .GetProperty(nameof(INestedAttributesService.PropertyWithPrivateGetter))!; + + prop.Should().NotBeNull(); + + prop.SetMethod.Should().NotBeNull(); + prop.GetMethod.Should().BeNull(); + + _sut.PropertyWithPrivateGetter = string.Empty; + } + + [Fact] + public void ProtectedSetter_IsOmitted() + { + var prop = typeof(INestedAttributesService) + .GetProperty(nameof(INestedAttributesService.PropertyWithProtectedSetter))!; + + prop.Should().NotBeNull(); + + prop.GetMethod.Should().NotBeNull(); + prop.SetMethod.Should().BeNull(); + + var _ = _sut.PropertyWithProtectedSetter; + } + + [Fact] + public void ProtectedGetter_IsOmitted() + { + var prop = typeof(INestedAttributesService) + .GetProperty(nameof(INestedAttributesService.PropertyWithProtectedGetter))!; + + prop.Should().NotBeNull(); + + prop.SetMethod.Should().NotBeNull(); + prop.GetMethod.Should().BeNull(); + + _sut.PropertyWithProtectedGetter = string.Empty; + } + + [Fact] + public void IgnoredProperty_IsOmitted() + { + var prop = typeof(INestedAttributesService) + .GetProperty(nameof(NestedAttributesService.IgnoredProperty)); + + prop.Should().BeNull(); + } + + [Fact] + public void StaticProperty_IsOmitted() + { + var prop = typeof(INestedAttributesService) + .GetProperty(nameof(NestedAttributesService.StaticProperty)); + + prop.Should().BeNull(); + } + + [Fact] + public void ImplementsIAutoInterface() + { + var interfaces = typeof(INestedAttributesService).GetInterfaces(); + interfaces.Should().HaveCount(1); + interfaces[0].Name.Should().Be(typeof(IAutoInterface).Name); + } +} + +public class NestedGenerateAttribute : GenerateAutoInterfaceAttribute +{ +} + +public class NestedIgnoreAttribute : AutoInterfaceIgnoreAttribute +{ +} + +// ReSharper disable UnusedMember.Local, ValueParameterNotUsed +[NestedGenerate] +public class NestedAttributesService : INestedAttributesService +{ + public int this[string x] + { + get => 0; + set + { + } + } + + public string PublicProperty { get; set; } + + public string InitOnlyProperty { get; init; } + + public string PropertyWithPrivateSetter { get; private set; } + + public string PropertyWithPrivateGetter { private get; set; } + + public string PropertyWithProtectedSetter { get; protected set; } + + public string PropertyWithProtectedGetter { protected get; set; } + + [NestedIgnore] public string IgnoredProperty { get; set; } + + public static string StaticProperty { get; set; } +} +// ReSharper enable UnusedMember.Local, ValueParameterNotUsed \ No newline at end of file diff --git a/InterfaceGenerator.sln b/InterfaceGenerator.sln index 3cae7ca..960d67c 100644 --- a/InterfaceGenerator.sln +++ b/InterfaceGenerator.sln @@ -1,8 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterfaceGenerator", "InterfaceGenerator\InterfaceGenerator.csproj", "{D40E2D60-5580-42D2-8316-F5AAA42CFBF6}" +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InterfaceGenerator.Tests", "InterfaceGenerator.Tests\InterfaceGenerator.Tests.csproj", "{D39C541E-9EDC-41E9-BBD8-FAA9DA602CC0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InterfaceGenerator.Contract", "InterfaceGenerator.Contract\InterfaceGenerator.Contract.csproj", "{51AC3DB1-FE08-4481-8DDF-52594FC17980}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -18,5 +23,15 @@ Global {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 + {51AC3DB1-FE08-4481-8DDF-52594FC17980}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51AC3DB1-FE08-4481-8DDF-52594FC17980}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51AC3DB1-FE08-4481-8DDF-52594FC17980}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51AC3DB1-FE08-4481-8DDF-52594FC17980}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {797494DA-A9BE-48BB-A5A0-6D1BD66754F5} EndGlobalSection EndGlobal diff --git a/InterfaceGenerator/AttributeDataExtensions.cs b/InterfaceGenerator/AttributeDataExtensions.cs index 084842a..7aa481e 100644 --- a/InterfaceGenerator/AttributeDataExtensions.cs +++ b/InterfaceGenerator/AttributeDataExtensions.cs @@ -1,14 +1,36 @@ -using System.Linq; using Microsoft.CodeAnalysis; namespace InterfaceGenerator { internal static class AttributeDataExtensions { - public static string? GetNamedParamValue(this AttributeData attributeData, string paramName) + public static TValue? GetParamValue(this AttributeData attributeData, string paramName) { - var pair = attributeData.NamedArguments.FirstOrDefault(x => x.Key == paramName); - return pair.Value.Value?.ToString(); + // Check constructor arguments + var constructor = attributeData.AttributeConstructor; + if (constructor != null) + { + var parameters = constructor.Parameters; + for (int i = 0; i < parameters.Length; i++) + { + if (parameters[i].Name == paramName) + { + var argument = attributeData.ConstructorArguments[i]; + return (TValue?)argument.Value; + } + } + } + + // Check named arguments + foreach (var arg in attributeData.NamedArguments) + { + if (arg.Key == paramName) + { + return (TValue?)arg.Value.Value; + } + } + + return default; } } } \ No newline at end of file diff --git a/InterfaceGenerator/Attributes.cs b/InterfaceGenerator/Attributes.cs deleted file mode 100644 index 754b8ec..0000000 --- a/InterfaceGenerator/Attributes.cs +++ /dev/null @@ -1,43 +0,0 @@ -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"")] - internal 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"")] - internal sealed class {AutoInterfaceIgnoreAttributeClassname} : Attribute - {{ - }} -}} -"; - } -} \ No newline at end of file diff --git a/InterfaceGenerator/AutoInterfaceGenerator.cs b/InterfaceGenerator/AutoInterfaceGenerator.cs index e4ec264..5ef725f 100644 --- a/InterfaceGenerator/AutoInterfaceGenerator.cs +++ b/InterfaceGenerator/AutoInterfaceGenerator.cs @@ -7,8 +7,10 @@ using System.Linq; using System.Text; using System.Threading; +using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace InterfaceGenerator @@ -17,7 +19,9 @@ namespace InterfaceGenerator public class AutoInterfaceGenerator : ISourceGenerator { private INamedTypeSymbol _generateAutoInterfaceAttribute = null!; + private INamedTypeSymbol _generateGenericAutoInterfaceAttribute = null!; private INamedTypeSymbol _ignoreAttribute = null!; + private INamedTypeSymbol _nameTemplateAttribute = null!; public void Initialize(GeneratorInitializationContext context) { @@ -47,7 +51,7 @@ public void Execute(GeneratorExecutionContext context) private static void RaiseExceptionDiagnostic(GeneratorExecutionContext context, Exception exception) { var descriptor = new DiagnosticDescriptor( - "InterfaceGenerator.CriticalError", + "IG0001", $"Exception thrown in InterfaceGenerator", $"{exception.GetType().FullName} {exception.Message} {exception.StackTrace.Trim()}", "InterfaceGenerator", @@ -67,19 +71,11 @@ private void ExecuteCore(GeneratorExecutionContext context) 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) @@ -96,7 +92,8 @@ private void GenerateInterfaces(GeneratorExecutionContext context) foreach (var implTypeSymbol in classSymbols) { - if (!implTypeSymbol.TryGetAttribute(_generateAutoInterfaceAttribute, out var attributes)) + if (!implTypeSymbol.TryGetAttribute(_generateAutoInterfaceAttribute, out var attributes) + && !implTypeSymbol.TryGetAttribute(_generateGenericAutoInterfaceAttribute, out attributes)) { continue; } @@ -108,7 +105,7 @@ private void GenerateInterfaces(GeneratorExecutionContext context) classSymbolNames.Add(implTypeSymbol.GetFullMetadataName(useNameWhenNotFound: true)); - var attribute = attributes.Single(); + var attribute = attributes.Last(); var source = SourceText.From(GenerateInterfaceCode(implTypeSymbol, attribute), Encoding.UTF8); context.AddSource($"{implTypeSymbol.GetFullMetadataName(useNameWhenNotFound: true)}_AutoInterface.g.cs", source); @@ -117,7 +114,7 @@ private void GenerateInterfaces(GeneratorExecutionContext context) private static string InferVisibilityModifier(ISymbol implTypeSymbol, AttributeData attributeData) { - string? result = attributeData.GetNamedParamValue(Attributes.VisibilityModifierPropName); + string? result = attributeData.GetParamValue(nameof(GenerateAutoInterfaceAttribute.VisibilityModifier)); if (!string.IsNullOrEmpty(result)) { return result!; @@ -130,9 +127,15 @@ private static string InferVisibilityModifier(ISymbol implTypeSymbol, AttributeD }; } - private static string InferInterfaceName(ISymbol implTypeSymbol, AttributeData attributeData) + private string InferInterfaceName(ISymbol implTypeSymbol, AttributeData attributeData) { - return attributeData.GetNamedParamValue(Attributes.InterfaceNamePropName) ?? $"I{implTypeSymbol.Name}"; + return attributeData.GetParamValue(nameof(GenerateAutoInterfaceAttribute.Name)) + ?? attributeData.GetParamValue(nameof(GenerateAutoInterfaceAttribute.NameTemplate))?.Replace("{Name}", implTypeSymbol.Name) + ?? (attributeData.AttributeClass?.TryGetAttribute(_nameTemplateAttribute, out var nameTemplateAttributes) == true + ? nameTemplateAttributes.First().GetParamValue( + nameof(AutoInterfaceNameTemplateAttribute.NameTemplate))!.Replace("{Name}", implTypeSymbol.Name) + : null) + ?? $"I{implTypeSymbol.Name}"; } private string GenerateInterfaceCode(INamedTypeSymbol implTypeSymbol, AttributeData attributeData) @@ -145,6 +148,8 @@ private string GenerateInterfaceCode(INamedTypeSymbol implTypeSymbol, AttributeD var interfaceName = InferInterfaceName(implTypeSymbol, attributeData); var visibilityModifier = InferVisibilityModifier(implTypeSymbol, attributeData); + codeWriter.WriteLine("// "); + codeWriter.WriteLine("#nullable enable"); codeWriter.WriteLine("namespace {0}", namespaceName); codeWriter.WriteLine("{"); @@ -152,6 +157,8 @@ private string GenerateInterfaceCode(INamedTypeSymbol implTypeSymbol, AttributeD WriteSymbolDocsIfPresent(codeWriter, implTypeSymbol); codeWriter.Write("{0} partial interface {1}", visibilityModifier, interfaceName); WriteTypeGenericsIfNeeded(codeWriter, implTypeSymbol); + WriteBaseInterface(codeWriter, attributeData, implTypeSymbol); + WriteTypeParameterConstraintsIfNeeded(codeWriter, implTypeSymbol); codeWriter.WriteLine(); codeWriter.WriteLine("{"); @@ -163,6 +170,7 @@ private string GenerateInterfaceCode(INamedTypeSymbol implTypeSymbol, AttributeD --codeWriter.Indent; codeWriter.WriteLine("}"); + codeWriter.WriteLine("#nullable restore"); codeWriter.Flush(); stream.Seek(0, SeekOrigin.Begin); @@ -170,6 +178,22 @@ private string GenerateInterfaceCode(INamedTypeSymbol implTypeSymbol, AttributeD return reader.ReadToEnd(); } + private void WriteBaseInterface(IndentedTextWriter codeWriter, AttributeData attributeData, INamedTypeSymbol implTypeSymbol) + { + if (implTypeSymbol.DeclaredAccessibility != Accessibility.Public) + return; + if (attributeData.AttributeClass!.Is(_generateAutoInterfaceAttribute)) + { + codeWriter.Write($" : {typeof(IAutoInterface).Namespace}.{nameof(IAutoInterface)}"); + } + else if (attributeData.AttributeClass!.Is(_generateGenericAutoInterfaceAttribute)) + { + codeWriter.Write($" : {typeof(IAutoInterface).Namespace}.{nameof(IAutoInterface)}<{implTypeSymbol.Name}"); + WriteTypeGenericsIfNeeded(codeWriter, implTypeSymbol); + codeWriter.Write(">"); + } + } + private static void WriteTypeGenericsIfNeeded(TextWriter writer, INamedTypeSymbol implTypeSymbol) { if (!implTypeSymbol.IsGenericType) @@ -180,16 +204,25 @@ private static void WriteTypeGenericsIfNeeded(TextWriter writer, INamedTypeSymbo writer.Write("<"); writer.WriteJoin(", ", implTypeSymbol.TypeParameters.Select(x => x.Name)); writer.Write(">"); + } + + private static void WriteTypeParameterConstraintsIfNeeded(TextWriter writer, INamedTypeSymbol implTypeSymbol) + { + if (!implTypeSymbol.IsGenericType) + { + return; + } WriteTypeParameterConstraints(writer, implTypeSymbol.TypeParameters); } - private void GenerateInterfaceMemberDefinitions(TextWriter writer, INamespaceOrTypeSymbol implTypeSymbol) + private void GenerateInterfaceMemberDefinitions(TextWriter writer, INamedTypeSymbol implTypeSymbol) { - foreach (var member in implTypeSymbol.GetMembers()) + foreach (var member in implTypeSymbol.GetAllMembers()) { - if (member.DeclaredAccessibility != Accessibility.Public || - member.HasAttribute(_ignoreAttribute)) + if (member.DeclaredAccessibility != Accessibility.Public + || member.HasAttribute(_ignoreAttribute) + || member.ContainingType.Name == nameof(Object)) { continue; } @@ -429,10 +462,16 @@ private static void WriteTypeParameterConstraints( private void InitAttributes(Compilation compilation) { _generateAutoInterfaceAttribute = compilation.GetTypeByMetadataName( - $"{Attributes.AttributesNamespace}.{Attributes.GenerateAutoInterfaceClassname}")!; - + $"{typeof(GenerateAutoInterfaceAttribute).Namespace}.{nameof(GenerateAutoInterfaceAttribute)}")!; + + _generateGenericAutoInterfaceAttribute = compilation.GetTypeByMetadataName( + $"{typeof(GenerateGenericAutoInterfaceAttribute).Namespace}.{nameof(GenerateGenericAutoInterfaceAttribute)}")!; + _ignoreAttribute = compilation.GetTypeByMetadataName( - $"{Attributes.AttributesNamespace}.{Attributes.AutoInterfaceIgnoreAttributeClassname}")!; + $"{typeof(AutoInterfaceIgnoreAttribute).Namespace}.{nameof(AutoInterfaceIgnoreAttribute)}")!; + + _nameTemplateAttribute = compilation.GetTypeByMetadataName( + $"{typeof(AutoInterfaceNameTemplateAttribute).Namespace}.{nameof(AutoInterfaceNameTemplateAttribute)}")!; } private static IEnumerable GetImplTypeSymbols(Compilation compilation, SyntaxReceiver receiver) @@ -449,12 +488,7 @@ private static INamedTypeSymbol GetTypeSymbol(Compilation compilation, SyntaxNod 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)); - + var compilation = context.Compilation; return compilation; } } diff --git a/InterfaceGenerator/InterfaceGenerator.csproj b/InterfaceGenerator/InterfaceGenerator.csproj index 9da3776..ca4b1d8 100644 --- a/InterfaceGenerator/InterfaceGenerator.csproj +++ b/InterfaceGenerator/InterfaceGenerator.csproj @@ -1,35 +1,49 @@ - - - netstandard2 - 9.0 - enable - 1.0.14 + + + netstandard2 + latest + enable + 1.0.14 + 1.0.14 - true - false - false - true - - - - R. David - InterfaceGenerator - A source generator that creates interfaces from implementations - https://github.com/daver32/InterfaceGenerator - https://github.com/daver32/InterfaceGenerator/blob/master/LICENSE - https://github.com/daver32/InterfaceGenerator - git - + false + false + true + true + - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + R. David + InterfaceGenerator + A source generator that creates interfaces from implementations + https://github.com/daver32/InterfaceGenerator + https://github.com/daver32/InterfaceGenerator/blob/master/LICENSE + https://github.com/daver32/InterfaceGenerator + git + $(AssemblyName) + - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + diff --git a/InterfaceGenerator/SymbolExtensions.cs b/InterfaceGenerator/SymbolExtensions.cs index 5f309ae..dfed2f7 100644 --- a/InterfaceGenerator/SymbolExtensions.cs +++ b/InterfaceGenerator/SymbolExtensions.cs @@ -12,15 +12,22 @@ public static bool TryGetAttribute( INamedTypeSymbol attributeType, out IEnumerable attributes) { - attributes = symbol.GetAttributes() - .Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); + attributes = symbol + .GetAttributes() + .Where(a => a.AttributeClass!.GetBaseTypesAndThis().Any(i => SymbolEqualityComparer.Default.Equals(i, attributeType))); return attributes.Any(); } public static bool HasAttribute(this ISymbol symbol, INamedTypeSymbol attributeType) { - return symbol.GetAttributes() - .Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); + return symbol + .GetAttributes() + .Any(a => a.AttributeClass!.GetBaseTypesAndThis().Any(i => SymbolEqualityComparer.Default.Equals(i, attributeType))); + } + + public static bool Is(this ITypeSymbol symbol, INamedTypeSymbol baseType) + { + return symbol.GetBaseTypesAndThis().Any(i => SymbolEqualityComparer.Default.Equals(i, baseType)); } //Ref: https://stackoverflow.com/questions/27105909/get-fully-qualified-metadata-name-in-roslyn @@ -64,5 +71,21 @@ private static bool IsRootNamespace(ISymbol symbol) { return symbol is INamespaceSymbol { IsGlobalNamespace: true }; } + + // Ref: https://github.com/dotnet/roslyn/blob/0c8ac4c91d0c61869a523433792691adab34242e/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ITypeSymbolExtensions.cs#L114 + 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(x => x.GetMembers()); + } } } \ No newline at end of file