Skip to content

Commit

Permalink
Introduce two new options (#286)
Browse files Browse the repository at this point in the history
  • Loading branch information
sungam3r committed Mar 6, 2023
1 parent 38edd64 commit eb72682
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 104 deletions.
180 changes: 98 additions & 82 deletions src/PublicApiGenerator/ApiGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using Microsoft.CSharp;
using Mono.Cecil;
using Mono.Cecil.Rocks;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Globalization;
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.CSharp;
using Mono.Cecil;
using Mono.Cecil.Rocks;
using ICustomAttributeProvider = Mono.Cecil.ICustomAttributeProvider;
using TypeAttributes = System.Reflection.TypeAttributes;

Expand Down Expand Up @@ -45,10 +45,12 @@ public static string GeneratePublicApi(this Assembly assembly, ApiGeneratorOptio
return CreatePublicApiForAssembly(
asm,
typeDefinition => !typeDefinition.IsNested &&
ShouldIncludeType(typeDefinition) &&
ShouldIncludeType(typeDefinition, options.BlacklistedNamespacePrefixes, options.WhitelistedNamespacePrefixes, options.UseBlacklistedNamespacePrefixesForExtensionMethods) &&
(options.IncludeTypes == null || options.IncludeTypes.Any(type => type.FullName == typeDefinition.FullName && type.Assembly.FullName == typeDefinition.Module.Assembly.FullName)),
options.IncludeAssemblyAttributes,
options.BlacklistedNamespacePrefixes,
options.WhitelistedNamespacePrefixes,
options.UseBlacklistedNamespacePrefixesForExtensionMethods,
attributeFilter);
}
}
Expand Down Expand Up @@ -104,90 +106,105 @@ public static string GeneratePublicApi(Assembly assembly, Type[]? includeTypes =

// TODO: Assembly references?
// TODO: Better handle namespaces - using statements? - requires non-qualified type names
private static string CreatePublicApiForAssembly(AssemblyDefinition assembly, Func<TypeDefinition, bool> shouldIncludeType, bool shouldIncludeAssemblyAttributes, string[] whitelistedNamespacePrefixes, AttributeFilter attributeFilter)
private static string CreatePublicApiForAssembly(AssemblyDefinition assembly, Func<TypeDefinition, bool> shouldIncludeType, bool shouldIncludeAssemblyAttributes, string[] blacklistedNamespacePrefixes, string[] whitelistedNamespacePrefixes, bool useBlacklistedNamespacePrefixesForExtensionMethods, AttributeFilter attributeFilter)
{
using (var provider = new CSharpCodeProvider())
using var provider = new CSharpCodeProvider();

var compileUnit = new CodeCompileUnit();
if (shouldIncludeAssemblyAttributes && assembly.HasCustomAttributes)
{
var compileUnit = new CodeCompileUnit();
if (shouldIncludeAssemblyAttributes && assembly.HasCustomAttributes)
PopulateCustomAttributes(assembly, compileUnit.AssemblyCustomAttributes, attributeFilter);
}

var publicTypes = assembly.Modules.SelectMany(m => m.GetTypes())
.Where(shouldIncludeType)
.OrderBy(t => t.FullName, StringComparer.Ordinal);
foreach (var publicType in publicTypes)
{
var @namespace = compileUnit.Namespaces.Cast<CodeNamespace>().FirstOrDefault(n => n.Name == publicType.Namespace);
if (@namespace == null)
{
PopulateCustomAttributes(assembly, compileUnit.AssemblyCustomAttributes, attributeFilter);
@namespace = new CodeNamespace(publicType.Namespace);
compileUnit.Namespaces.Add(@namespace);
}

var publicTypes = assembly.Modules.SelectMany(m => m.GetTypes())
.Where(shouldIncludeType)
.OrderBy(t => t.FullName, StringComparer.Ordinal);
foreach (var publicType in publicTypes)
using (NullableContext.Push(publicType))
{
var @namespace = compileUnit.Namespaces.Cast<CodeNamespace>()
.FirstOrDefault(n => n.Name == publicType.Namespace);
if (@namespace == null)
{
@namespace = new CodeNamespace(publicType.Namespace);
compileUnit.Namespaces.Add(@namespace);
}

using (NullableContext.Push(publicType))
{
var typeDeclaration = CreateTypeDeclaration(publicType, whitelistedNamespacePrefixes, attributeFilter);
@namespace.Types.Add(typeDeclaration);
}
var typeDeclaration = CreateTypeDeclaration(publicType, blacklistedNamespacePrefixes, whitelistedNamespacePrefixes, useBlacklistedNamespacePrefixesForExtensionMethods, attributeFilter);
@namespace.Types.Add(typeDeclaration);
}
}

using (var writer = new StringWriter())
using (var writer = new StringWriter())
{
var cgo = new CodeGeneratorOptions
{
var cgo = new CodeGeneratorOptions
{
BracingStyle = "C",
BlankLinesBetweenMembers = false,
VerbatimOrder = false,
IndentString = " "
};

provider.GenerateCodeFromCompileUnit(compileUnit, writer, cgo);
return CodeNormalizer.NormalizeGeneratedCode(writer);
}
BracingStyle = "C",
BlankLinesBetweenMembers = false,
VerbatimOrder = false,
IndentString = " "
};

provider.GenerateCodeFromCompileUnit(compileUnit, writer, cgo);
return CodeNormalizer.NormalizeGeneratedCode(writer);
}
}

private static bool ShouldIncludeType(TypeDefinition t)
private static bool ShouldIncludeType(TypeDefinition t, string[] blacklistedNamespacePrefixes, string[] whitelistedNamespacePrefixes, bool useBlacklistedNamespacePrefixesForExtensionMethods)
{
return (t.IsPublic || t.IsNestedPublic || t.IsNestedFamily || t.IsNestedFamilyOrAssembly) && !t.IsCompilerGenerated();
if (t.IsCompilerGenerated())
return false;

if (!t.IsPublic && !t.IsNestedPublic && !t.IsNestedFamily && !t.IsNestedFamilyOrAssembly)
return false;

if (!useBlacklistedNamespacePrefixesForExtensionMethods)
{
if (t.GetMembers().Any(m => ShouldIncludeMember(m, blacklistedNamespacePrefixes, whitelistedNamespacePrefixes, useBlacklistedNamespacePrefixesForExtensionMethods)))
return true;
}

if (blacklistedNamespacePrefixes.Any(t.FullName.StartsWith)
&& !whitelistedNamespacePrefixes.Any(t.FullName.StartsWith))
return false;

return true;
}

private static bool ShouldIncludeMember(IMemberDefinition m, string[] whitelistedNamespacePrefixes)
private static bool ShouldIncludeMember(IMemberDefinition m, string[] blacklistedNamespacePrefixes, string[] whitelistedNamespacePrefixes, bool useBlacklistedNamespacePrefixesForExtensionMethods)
{
// https://github.com/PublicApiGenerator/PublicApiGenerator/issues/245
bool isRecord = m.DeclaringType.GetMethods().Any(m => m.Name == "<Clone>$");
if (isRecord && m.Name == "EqualityContract")
return false;

return !m.IsCompilerGenerated() && !IsDotNetTypeMember(m, whitelistedNamespacePrefixes) && !(m is FieldDefinition);
}
if (m.IsCompilerGenerated())
return false;

private static bool ShouldIncludeMember(MemberAttributes memberAttributes)
{
switch (memberAttributes & MemberAttributes.AccessMask)
{
case 0: // This represents no CodeDOM keyword being specified.
case MemberAttributes.Private:
case MemberAttributes.Assembly:
case MemberAttributes.FamilyAndAssembly:
return false;
default:
return true;
}
}
if (m is FieldDefinition)
return false;

private static bool IsDotNetTypeMember(IMemberDefinition m, string[] whitelistedNamespacePrefixes)
{
if (m.DeclaringType?.FullName == null)
return false;

return (m.DeclaringType.FullName.StartsWith("System") || m.DeclaringType.FullName.StartsWith("Microsoft"))
&& !whitelistedNamespacePrefixes.Any(prefix => m.DeclaringType.FullName.StartsWith(prefix));
if (!useBlacklistedNamespacePrefixesForExtensionMethods && m is MethodDefinition md && md.IsExtensionMethod())
return true;

if (blacklistedNamespacePrefixes.Any(m.DeclaringType.FullName.StartsWith)
&& !whitelistedNamespacePrefixes.Any(m.DeclaringType.FullName.StartsWith))
return false;

return true;
}

private static bool ShouldIncludeMember(MemberAttributes memberAttributes)
=> (memberAttributes & MemberAttributes.AccessMask) switch
{
// 0 represents no CodeDOM keyword being specified.
0 or MemberAttributes.Private or MemberAttributes.Assembly or MemberAttributes.FamilyAndAssembly => false,
_ => true,
};

private static void AddMemberToTypeDeclaration(CodeTypeDeclaration typeDeclaration,
IMemberDefinition typeDeclarationInfo,
IMemberDefinition memberInfo,
Expand Down Expand Up @@ -217,7 +234,7 @@ private static bool IsDotNetTypeMember(IMemberDefinition m, string[] whitelisted
}
}

private static CodeTypeDeclaration CreateTypeDeclaration(TypeDefinition publicType, string[] whitelistedNamespacePrefixes, AttributeFilter attributeFilter)
private static CodeTypeDeclaration CreateTypeDeclaration(TypeDefinition publicType, string[] blacklistedNamespacePrefixes, string[] whitelistedNamespacePrefixes, bool useBlacklistedNamespacePrefixesForExtensionMethods, AttributeFilter attributeFilter)
{
if (publicType.IsDelegate())
return CreateDelegateDeclaration(publicType, attributeFilter);
Expand Down Expand Up @@ -298,11 +315,11 @@ private static CodeTypeDeclaration CreateTypeDeclaration(TypeDefinition publicTy
}
foreach (var @interface in publicType.Interfaces.OrderBy(i => i.InterfaceType.FullName, StringComparer.Ordinal)
.Select(t => new { Reference = t, Definition = t.InterfaceType.Resolve() })
.Where(t => ShouldIncludeType(t.Definition))
.Where(t => ShouldIncludeType(t.Definition, Array.Empty<string>(), Array.Empty<string>(), true))
.Select(t => t.Reference))
declaration.BaseTypes.Add(@interface.InterfaceType.CreateCodeTypeReference(@interface));

foreach (var memberInfo in publicType.GetMembers().Where(memberDefinition => ShouldIncludeMember(memberDefinition, whitelistedNamespacePrefixes)).OrderBy(m => m.Name, StringComparer.Ordinal))
foreach (var memberInfo in publicType.GetMembers().Where(memberDefinition => ShouldIncludeMember(memberDefinition, blacklistedNamespacePrefixes, whitelistedNamespacePrefixes, useBlacklistedNamespacePrefixesForExtensionMethods)).OrderBy(m => m.Name, StringComparer.Ordinal))
AddMemberToTypeDeclaration(declaration, publicType, memberInfo, attributeFilter);

// Fields should be in defined order for an enum
Expand All @@ -312,11 +329,11 @@ private static CodeTypeDeclaration CreateTypeDeclaration(TypeDefinition publicTy
foreach (var field in fields)
AddMemberToTypeDeclaration(declaration, publicType, field, attributeFilter);

foreach (var nestedType in publicType.NestedTypes.Where(ShouldIncludeType).OrderBy(t => t.FullName, StringComparer.Ordinal))
foreach (var nestedType in publicType.NestedTypes.Where(t => ShouldIncludeType(t, blacklistedNamespacePrefixes, whitelistedNamespacePrefixes, useBlacklistedNamespacePrefixesForExtensionMethods)).OrderBy(t => t.FullName, StringComparer.Ordinal))
{
using (NullableContext.Push(nestedType))
{
var nestedTypeDeclaration = CreateTypeDeclaration(nestedType, whitelistedNamespacePrefixes, attributeFilter);
var nestedTypeDeclaration = CreateTypeDeclaration(nestedType, blacklistedNamespacePrefixes, whitelistedNamespacePrefixes, useBlacklistedNamespacePrefixesForExtensionMethods, attributeFilter);
declaration.Members.Add(nestedTypeDeclaration);
}
}
Expand Down Expand Up @@ -488,24 +505,23 @@ private static CodeAttributeDeclaration GenerateCodeAttributeDeclaration(Func<Co
// Litee: This method is used for additional sorting of custom attributes when multiple values are allowed
private static string ConvertAttributeToCode(Func<CodeTypeReference, CodeTypeReference> codeTypeModifier, CustomAttribute customAttribute)
{
using (var provider = new CSharpCodeProvider())
using var provider = new CSharpCodeProvider();

var cgo = new CodeGeneratorOptions
{
var cgo = new CodeGeneratorOptions
{
BracingStyle = "C",
BlankLinesBetweenMembers = false,
VerbatimOrder = false
};
var attribute = GenerateCodeAttributeDeclaration(codeTypeModifier, customAttribute);
var declaration = new CodeTypeDeclaration("DummyClass")
{
CustomAttributes = new CodeAttributeDeclarationCollection(new[] { attribute }),
};
using (var writer = new StringWriter())
{
provider.GenerateCodeFromType(declaration, writer, cgo);
return writer.ToString();
}
BracingStyle = "C",
BlankLinesBetweenMembers = false,
VerbatimOrder = false
};
var attribute = GenerateCodeAttributeDeclaration(codeTypeModifier, customAttribute);
var declaration = new CodeTypeDeclaration("DummyClass")
{
CustomAttributes = new CodeAttributeDeclarationCollection(new[] { attribute }),
};
using (var writer = new StringWriter())
{
provider.GenerateCodeFromType(declaration, writer, cgo);
return writer.ToString();
}
}

Expand Down
42 changes: 35 additions & 7 deletions src/PublicApiGenerator/ApiGeneratorOptions.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,59 @@
namespace PublicApiGenerator;

/// <summary>
/// Options to influence the ApiGenerator
/// Options to influence the ApiGenerator output.
/// </summary>
public class ApiGeneratorOptions
{
/// <summary>
/// Allows to control which types of the generated assembly should be included. If this option is specified all other types found in the assembly that are not present here will be exclude.
/// Allows to control which types of the generated assembly should be included.
/// If this option is specified all other types found in the assembly that are not present here will be excluded.
/// </summary>
public Type[]? IncludeTypes { get; set; }

/// <summary>
/// Instructs the generator to include assembly level attributes.
/// </summary>
/// <remarks>Defaults to true</remarks>
/// <remarks>Defaults to <see langword="true"/>.</remarks>
public bool IncludeAssemblyAttributes { get; set; } = true;

/// <summary>
/// Allows to whitelist certain namespace prefixes. For example by default types found in Microsoft or System namespaces are not treated as part of the public API.
/// Allows to whitelist certain namespace prefixes.
/// For example by default types found in Microsoft or System namespaces are not treated as part of the public API.
/// This option has priority over <see cref="BlacklistedNamespacePrefixes"/>.
/// </summary>
/// <example>
/// <code>
/// var options = new DefaultApiGeneratorOptions
/// {
/// WhitelistedNamespacePrefixes = new[] {"Microsoft.Whitelisted" }
/// WhitelistedNamespacePrefixes = new[] { "Microsoft.Whitelisted" }
/// };
/// </code>
/// </example>
public string[] WhitelistedNamespacePrefixes { get; set; } = DefaultWhitelistedNamespacePrefixes;
public string[] WhitelistedNamespacePrefixes { get; set; } = _defaultWhitelistedNamespacePrefixes;

/// <summary>
/// Allows to blacklist certain namespace prefixes.
/// By default types found in Microsoft or System namespaces are not treated as part of the public API.
/// </summary>
/// <example>
/// <code>
/// var options = new DefaultApiGeneratorOptions
/// {
/// BlacklistedNamespacePrefixes = new[] { "System", "Microsoft", "ThirdParty" }
/// };
/// </code>
/// </example>
public string[] BlacklistedNamespacePrefixes { get; set; } = _defaultBlacklistedNamespacePrefixes;

/// <summary>
/// Allows to control whether to include extension methods into generated API even when
/// containing class falls into <see cref="BlacklistedNamespacePrefixes"/> option. This
/// option may be useful, for example, for those who writes extensions for IServiceCollection
/// keeping them into Microsoft.Extensions.DependencyInjection namespace for better discoverability.
/// </summary>
/// <remarks>Defaults to <see langword="true"/>, i.e. extension methods are excluded from output.</remarks>
public bool UseBlacklistedNamespacePrefixesForExtensionMethods { get; set; } = true;

/// <summary>
/// Allows to exclude attributes by specifying the fullname of the attribute to exclude.
Expand All @@ -42,5 +68,7 @@ public class ApiGeneratorOptions
/// </example>
public string[]? ExcludeAttributes { get; set; }

static readonly string[] DefaultWhitelistedNamespacePrefixes = Array.Empty<string>();
private static readonly string[] _defaultWhitelistedNamespacePrefixes = Array.Empty<string>();

private static readonly string[] _defaultBlacklistedNamespacePrefixes = new[] { "System", "Microsoft" };
}
29 changes: 29 additions & 0 deletions src/PublicApiGeneratorTests/Method_extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ public static class StringExtensions
}");
}

[Fact]
public void Should_output_extension_methods_when_allowed()
{
AssertPublicApi(typeof(StringExtensionsInSystemNamespace),
@"namespace System
{
public static class StringExtensionsInSystemNamespace
{
public static bool CheckLength(this string value, int length) { }
}
}", new PublicApiGenerator.ApiGeneratorOptions { UseBlacklistedNamespacePrefixesForExtensionMethods = false, IncludeAssemblyAttributes = false });
}

[Fact]
public void Should_output_generic_extension_methods()
{
Expand Down Expand Up @@ -88,3 +101,19 @@ public static class ExtensionMethodWithNullable
// ReSharper restore ClassNeverInstantiated.Global
// ReSharper restore UnusedMember.Global
}

namespace System
{
public static class StringExtensionsInSystemNamespace
{
public static bool CheckLength(this string value, int length)
{
return value.Length == length;
}

public static bool CheckLengthNotAsExtension(string value, int length)
{
return value.Length == length;
}
}
}
Loading

0 comments on commit eb72682

Please sign in to comment.