diff --git a/src/PublicApiGenerator/ApiGenerator.cs b/src/PublicApiGenerator/ApiGenerator.cs index 178aa32..1594d68 100644 --- a/src/PublicApiGenerator/ApiGenerator.cs +++ b/src/PublicApiGenerator/ApiGenerator.cs @@ -180,6 +180,7 @@ static bool IsDotNetTypeMember(IMemberDefinition m, string[] whitelistedNamespac } static void AddMemberToTypeDeclaration(CodeTypeDeclaration typeDeclaration, + IMemberDefinition typeDeclarationInfo, IMemberDefinition memberInfo, AttributeFilter attributeFilter) { @@ -194,7 +195,7 @@ static bool IsDotNetTypeMember(IMemberDefinition m, string[] whitelistedNamespac } else if (memberInfo is PropertyDefinition propertyDefinition) { - AddPropertyToTypeDeclaration(typeDeclaration, propertyDefinition, attributeFilter); + AddPropertyToTypeDeclaration(typeDeclaration, typeDeclarationInfo, propertyDefinition, attributeFilter); } else if (memberInfo is EventDefinition eventDefinition) { @@ -293,14 +294,14 @@ static CodeTypeDeclaration CreateTypeDeclaration(TypeDefinition publicType, stri declaration.BaseTypes.Add(@interface.InterfaceType.CreateCodeTypeReference(@interface)); foreach (var memberInfo in publicType.GetMembers().Where(memberDefinition => ShouldIncludeMember(memberDefinition, whitelistedNamespacePrefixes)).OrderBy(m => m.Name, StringComparer.Ordinal)) - AddMemberToTypeDeclaration(declaration, memberInfo, attributeFilter); + AddMemberToTypeDeclaration(declaration, publicType, memberInfo, attributeFilter); // Fields should be in defined order for an enum var fields = !publicType.IsEnum ? publicType.Fields.OrderBy(f => f.Name, StringComparer.Ordinal) : (IEnumerable)publicType.Fields; foreach (var field in fields) - AddMemberToTypeDeclaration(declaration, field, attributeFilter); + AddMemberToTypeDeclaration(declaration, publicType, field, attributeFilter); foreach (var nestedType in publicType.NestedTypes.Where(ShouldIncludeType).OrderBy(t => t.FullName, StringComparer.Ordinal)) { @@ -445,10 +446,7 @@ bool IsSpecialConstraint(GenericParameterConstraint constraint) static CodeAttributeDeclaration GenerateCodeAttributeDeclaration(Func codeTypeModifier, CustomAttribute customAttribute) { var attribute = new CodeAttributeDeclaration(codeTypeModifier(customAttribute.AttributeType.CreateCodeTypeReference(mode: NullableMode.Disable))); - if (attribute.Name != "System.ParamArrayAttribute") - { - attribute.Name = $"{attribute.Name}{CodeNormalizer.AttributeMarker}"; - } + attribute.Name = AttributeNameBuilder.Get(attribute.Name); foreach (var arg in customAttribute.ConstructorArguments) { attribute.Arguments.Add(new CodeAttributeArgument(CreateInitialiserExpression(arg))); @@ -658,7 +656,7 @@ static object FormatParameterConstant(ParameterDefinition parameter) return parameter.ParameterType.IsValueType ? "default" : "null"; } - static void AddPropertyToTypeDeclaration(CodeTypeDeclaration typeDeclaration, PropertyDefinition member, AttributeFilter attributeFilter) + static void AddPropertyToTypeDeclaration(CodeTypeDeclaration typeDeclaration, IMemberDefinition typeDeclarationInfo, PropertyDefinition member, AttributeFilter attributeFilter) { var getterAttributes = member.GetMethod?.GetMethodAttributes() ?? 0; var setterAttributes = member.SetMethod?.GetMethodAttributes() ?? 0; @@ -675,9 +673,10 @@ static void AddPropertyToTypeDeclaration(CodeTypeDeclaration typeDeclaration, Pr ? new CodeTypeReference(member.PropertyType.Name) : member.PropertyType.CreateCodeTypeReference(member); + var propertyName = member.Name; var property = new CodeMemberProperty { - Name = member.Name, + Name = propertyName, Type = propertyType, Attributes = propertyAttributes, CustomAttributes = CreateCustomAttributes(member, attributeFilter), @@ -685,6 +684,21 @@ static void AddPropertyToTypeDeclaration(CodeTypeDeclaration typeDeclaration, Pr HasSet = hasSet }; + // DefaultMemberAttribute on type gets propagated to IndexerNameAttribute + var defaultMemberAttributeValue = typeDeclarationInfo.CustomAttributes.SingleOrDefault(x => + x.AttributeType.FullName == "System.Reflection.DefaultMemberAttribute") + ?.ConstructorArguments.Select(x => x.Value).OfType().SingleOrDefault(); + if (!string.IsNullOrEmpty(defaultMemberAttributeValue) && propertyName != "Item") + { + property.Name = "Item"; + property.CustomAttributes.Add( + new CodeAttributeDeclaration( + AttributeNameBuilder.Get("System.Runtime.CompilerServices.IndexerNameAttribute")) + { + Arguments = {new CodeAttributeArgument(new CodePrimitiveExpression(defaultMemberAttributeValue))} + }); + } + // Here's a nice hack, because hey, guess what, the CodeDOM doesn't support // attributes on getters or setters if (member.GetMethod != null && member.GetMethod.HasCustomAttributes) diff --git a/src/PublicApiGenerator/AttributeNameBuilder.cs b/src/PublicApiGenerator/AttributeNameBuilder.cs new file mode 100644 index 0000000..ede2d41 --- /dev/null +++ b/src/PublicApiGenerator/AttributeNameBuilder.cs @@ -0,0 +1,11 @@ +namespace PublicApiGenerator +{ + public static class AttributeNameBuilder + { + public static string Get(string name) + { + // ParamArrayAttribute cannot be augment with the attribute marker, it would trip up CodeDom + return name == "System.ParamArrayAttribute" ? name : $"{name}{CodeNormalizer.AttributeMarker}"; + } + } +} diff --git a/src/PublicApiGeneratorTests/Indexer_properties.cs b/src/PublicApiGeneratorTests/Indexer_properties.cs new file mode 100644 index 0000000..50a78a3 --- /dev/null +++ b/src/PublicApiGeneratorTests/Indexer_properties.cs @@ -0,0 +1,86 @@ +using System.Runtime.CompilerServices; +using PublicApiGeneratorTests.Examples; +using Xunit; + +namespace PublicApiGeneratorTests +{ + public class Indexer_properties : ApiGeneratorTestsBase + { + [Fact] + public void Should_output_indexer() + { + AssertPublicApi( + @"namespace PublicApiGeneratorTests.Examples +{ + public class ClassWithIndexer + { + public ClassWithIndexer() { } + public int this[int x] { get; } + } +}"); + } + + [Fact] + public void Should_output_named_indexer() + { + AssertPublicApi( + @"namespace PublicApiGeneratorTests.Examples +{ + public class ClassWithNamedIndexer + { + public ClassWithNamedIndexer() { } + [System.Runtime.CompilerServices.IndexerName(""Bar"")] + public int this[int x] { get; } + } +}"); + } + + [Fact] + public void Should_output_named_indexer_with_getset() + { + AssertPublicApi( + @"namespace PublicApiGeneratorTests.Examples +{ + public class ClassWithNamedIndexerGetSet + { + public ClassWithNamedIndexerGetSet() { } + [System.Runtime.CompilerServices.IndexerName(""Bar"")] + public int this[int x] { get; set; } + } +}"); + } + } + + + // ReSharper disable ClassNeverInstantiated.Global + // ReSharper disable UnusedMember.Global + // ReSharper disable ValueParameterNotUsed + namespace Examples + { + public class ClassWithIndexer + { + public int this[int x] => x; + } + + public class ClassWithNamedIndexer + { + [IndexerName("Bar")] + public int this[int x] => x; + } + + public class ClassWithNamedIndexerGetSet + { + private int y; + + [IndexerName("Bar")] + public int this[int x] + { + get => y; + set => y = value; + } + } + } + // ReSharper restore ValueParameterNotUsed + // ReSharper restore UnusedMember.Global + // ReSharper restore ClassNeverInstantiated.Global +}