Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C# 8 nullable reference type support #115

Merged
merged 1 commit into from Nov 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions .editorconfig
@@ -0,0 +1,16 @@
# EditorConfig is awesome: http://EditorConfig.org

# top-most EditorConfig file
root = true

# Editor default newlines with a newline ending every file
[*]
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*.cs]
indent_size = 4
indent_style = space
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -5,6 +5,7 @@ _[![Build status](https://github.com/JakeGinnivan/ApiApprover/workflows/.github/
## PublicApiGenerator

PublicApiGenerator has no dependencies and simply creates a string the represents the public API. Any approval library can be used to approve the generated public API.
PublicApiGenerator supports C# 8 [Nullable Reference Types](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references) from version 10.

## How do I use it

Expand Down
8 changes: 6 additions & 2 deletions src/ApiApprover.sln
Expand Up @@ -9,8 +9,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublicApiGenerator", "Publi
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{52361C5B-F247-42C5-8455-BD1DEDB5C903}"
ProjectSection(SolutionItems) = preProject
..\appveyor.yml = ..\appveyor.yml
..\build.ps1 = ..\build.ps1
..\.editorconfig = ..\.editorconfig
..\.gitattributes = ..\.gitattributes
..\.gitignore = ..\.gitignore
..\build.cmd = ..\build.cmd
..\build.sh = ..\build.sh
..\.github\workflows\ci.yml = ..\.github\workflows\ci.yml
Directory.Build.props = Directory.Build.props
..\LICENSE = ..\LICENSE
PublicApiGenerator.snk = PublicApiGenerator.snk
Expand Down
1 change: 1 addition & 0 deletions src/Directory.Build.props
Expand Up @@ -7,6 +7,7 @@
<Copyright>Copyright Jake Ginnivan 2010-$([System.DateTime]::UtcNow.ToString(yyyy)). All rights reserved.</Copyright>
<PackageTags>semanticversioning versioning api</PackageTags>
<PackageOutputPath>..\..\nugets</PackageOutputPath>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<PropertyGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/PublicApiGenerator.Tool/Program.cs
Expand Up @@ -250,7 +250,7 @@ public static void Main(string[] args)
var asm = Assembly.LoadFile(fullPath);
File.WriteAllText(outputPath, ApiGenerator.GeneratePublicApi(asm));
var destinationFilePath = Path.Combine(outputDirectory, Path.GetFileName(outputPath));
if(File.Exists(destinationFilePath))
if (File.Exists(destinationFilePath))
{
File.Delete(destinationFilePath);
}
Expand All @@ -259,4 +259,4 @@ public static void Main(string[] args)
}
";
}
}
}
239 changes: 130 additions & 109 deletions src/PublicApiGenerator/ApiGenerator.cs

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions src/PublicApiGenerator/CSharpAlias.cs
@@ -0,0 +1,42 @@
namespace PublicApiGenerator
{
internal static class CSharpAlias
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a fan of this change. 'Alias' means something specific in C#, so I'd call it CSharpTypeKeyword or something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe

{
public static string Get(string typeName)
{
switch (typeName)
{
case "System.Byte":
return "byte";
case "System.SByte":
return "sbyte";
case "System.Int16":
return "short";
case "System.UInt16":
return "ushort";
case "System.Int32":
return "int";
case "System.UInt32":
return "uint";
case "System.Int64":
return "long";
case "System.UInt64":
return "ulong";
case "System.Single":
return "float";
case "System.Double":
return "double";
case "System.Decimal":
return "decimal";
case "System.Object":
return "object";
case "System.String":
return "string";
case "System.Boolean":
return "bool";
default:
return typeName;
}
}
}
}
156 changes: 156 additions & 0 deletions src/PublicApiGenerator/CodeTypeReferenceBuilder.cs
@@ -0,0 +1,156 @@
using Mono.Cecil;
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Linq;

namespace PublicApiGenerator
{
internal static class CodeTypeReferenceBuilder
{
private const int MAX_COUNT = 100;

internal static CodeTypeReference CreateCodeTypeReference(this TypeReference type, ICustomAttributeProvider attributeProvider = null, NullableMode mode = NullableMode.Default)
{
return CreateCodeTypeReferenceWithNullabilityMap(type, attributeProvider.GetNullabilityMap().GetEnumerator(), mode, false);
}

static CodeTypeReference CreateCodeTypeReferenceWithNullabilityMap(TypeReference type, IEnumerator<bool?> nullabilityMap, NullableMode mode, bool disableNested)
{
var typeName = GetTypeName(type, nullabilityMap, mode, disableNested);
if (type.IsValueType && type.Name == "Nullable`1" && type.Namespace == "System")
{
// unwrap System.Nullable<Type> into Type? for readability
var genericArgs = type is IGenericInstance instance ? instance.GenericArguments : type.HasGenericParameters ? type.GenericParameters.Cast<TypeReference>() : null;
return CreateCodeTypeReferenceWithNullabilityMap(genericArgs.Single(), nullabilityMap, NullableMode.Force, disableNested);
}
else
{
return new CodeTypeReference(typeName, CreateGenericArguments(type, nullabilityMap));
}
}

static CodeTypeReference[] CreateGenericArguments(TypeReference type, IEnumerator<bool?> nullabilityMap)
{
// ReSharper disable once RedundantEnumerableCastCall
var genericArgs = type is IGenericInstance instance ? instance.GenericArguments : type.HasGenericParameters ? type.GenericParameters.Cast<TypeReference>() : null;
if (genericArgs == null) return null;

var genericArguments = new List<CodeTypeReference>();
foreach (var argument in genericArgs)
{
genericArguments.Add(CreateCodeTypeReferenceWithNullabilityMap(argument, nullabilityMap, NullableMode.Default, false));
}
return genericArguments.ToArray();
}

internal static IEnumerable<bool?> GetNullabilityMap(this ICustomAttributeProvider attributeProvider)
{
var nullableAttr = attributeProvider?.CustomAttributes.SingleOrDefault(d => d.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute");

if (nullableAttr == null)
{
foreach (var provider in NullableContext.Providers)
{
nullableAttr = provider.CustomAttributes.SingleOrDefault(d => d.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute");
if (nullableAttr != null)
break;
}
}

if (nullableAttr == null)
return Enumerable.Repeat((bool?)null, MAX_COUNT);

var value = nullableAttr.ConstructorArguments[0].Value;
if (value is CustomAttributeArgument[] arguments)
return arguments.Select(a => Convert((byte)a.Value));

return Enumerable.Repeat(Convert((byte)value), MAX_COUNT);

// https://github.com/dotnet/roslyn/blob/master/docs/features/nullable-metadata.md
// returns:
// true : explicitly nullable
// false: explicitly not nullable
// null : oblivious
bool? Convert(byte value)
{
switch (value)
{
case 2: return true;
case 1: return false;
case 0: return null;
default: throw new NotSupportedException(value.ToString());
}
}
}

// The compiler optimizes the size of metadata bypassing a sequence of bytes for value types.
// Thus, it can even delete the entire NullableAttribute if the whole signature consists only of value types,
// for example KeyValuePair<int, int?>, thus we can call IsNullable() only by looking first deep into the signature
private static bool HasAnyReferenceType(TypeReference type)
{
if (!type.IsValueType)
return true;

// ReSharper disable once RedundantEnumerableCastCall
var genericArgs = type is IGenericInstance instance ? instance.GenericArguments : type.HasGenericParameters ? type.GenericParameters.Cast<TypeReference>() : null;
if (genericArgs == null) return false;

foreach (var argument in genericArgs)
{
if (HasAnyReferenceType(argument))
return true;
}

return false;
}

static string GetTypeName(TypeReference type, IEnumerator<bool?> nullabilityMap, NullableMode mode, bool disableNested)
{
bool nullable = mode != NullableMode.Disable && (mode == NullableMode.Force || HasAnyReferenceType(type) && IsNullable());

var typeName = GetTypeNameCore(type, nullabilityMap, nullable, disableNested);

if (nullable && typeName != "System.Void")
typeName = CSharpAlias.Get(typeName) + "?";

return typeName;

bool IsNullable()
{
if (nullabilityMap == null)
return false;

if (!nullabilityMap.MoveNext())
{
throw new InvalidOperationException("Not enough nullability information");
}
return nullabilityMap.Current == true;
}
}

static string GetTypeNameCore(TypeReference type, IEnumerator<bool?> nullabilityMap, bool nullable, bool disableNested)
{
if (type.IsGenericParameter)
{
return type.Name;
}

if (type is ArrayType array)
{
if (nullable)
return CSharpAlias.Get(GetTypeName(array.ElementType, nullabilityMap, NullableMode.Default, disableNested)) + "[]";
else
return GetTypeName(array.ElementType, nullabilityMap, NullableMode.Default, disableNested) + "[]";
}

if (!type.IsNested || disableNested)
{
var name = type is RequiredModifierType modType ? modType.ElementType.Name : type.Name;
return (!string.IsNullOrEmpty(type.Namespace) ? (type.Namespace + ".") : "") + name;
}

return GetTypeName(type.DeclaringType, null, NullableMode.Default, false) + "." + GetTypeName(type, null, NullableMode.Default, true);
}
}
}
40 changes: 40 additions & 0 deletions src/PublicApiGenerator/NullableContext.cs
@@ -0,0 +1,40 @@
using Mono.Cecil;
using System;
using System.Collections.Generic;

namespace PublicApiGenerator
{
// https://github.com/dotnet/roslyn/blob/master/docs/features/nullable-metadata.md#nullablecontextattribute
internal class NullableContext
{
[ThreadStatic]
private static Stack<ICustomAttributeProvider> _nullableContextProviders;

private static Stack<ICustomAttributeProvider> NullableContextProviders
{
get
{
if (_nullableContextProviders == null)
_nullableContextProviders = new Stack<ICustomAttributeProvider>();
return _nullableContextProviders;
}
}

internal static IDisposable Push(ICustomAttributeProvider provider) => new PopDisposable(provider);

internal static IEnumerable<ICustomAttributeProvider> Providers => NullableContextProviders;

private sealed class PopDisposable : IDisposable
{
public PopDisposable(ICustomAttributeProvider provider)
{
NullableContextProviders.Push(provider);
}

public void Dispose()
{
NullableContextProviders.Pop();
}
}
}
}
9 changes: 9 additions & 0 deletions src/PublicApiGenerator/NullableMode.cs
@@ -0,0 +1,9 @@
namespace PublicApiGenerator
{
enum NullableMode
{
Default,
Force,
Disable
}
}
2 changes: 1 addition & 1 deletion src/PublicApiGenerator/PublicApiGenerator.csproj
Expand Up @@ -10,7 +10,7 @@

<ItemGroup>
<PackageReference Include="MinVer" Version="1.1.0" PrivateAssets="All" />
<PackageReference Include="Mono.Cecil" Version="0.10.4" />
<PackageReference Include="Mono.Cecil" Version="0.11.0" />
<PackageReference Include="System.CodeDom" Version="4.5.0" />
</ItemGroup>

Expand Down
4 changes: 2 additions & 2 deletions src/PublicApiGeneratorTests/ApiGeneratorTestsBase.cs
Expand Up @@ -19,9 +19,9 @@ protected void AssertPublicApi(Type type, string expectedOutput, bool includeAss
AssertPublicApi(new[] { type }, expectedOutput, includeAssemblyAttributes, excludeAttributes: excludeAttributes);
}

protected void AssertPublicApi(Type[] types, string expectedOutput, bool includeAssemblyAttributes = false, string[] whitelistedNamespacePrefixes = default(string[]), string[] excludeAttributes = null)
protected void AssertPublicApi(Type[] types, string expectedOutput, bool includeAssemblyAttributes = false, string[] whitelistedNamespacePrefixes = default, string[] excludeAttributes = null)
{
AssertPublicApi(GetType().Assembly, types, expectedOutput, includeAssemblyAttributes, whitelistedNamespacePrefixes, excludeAttributes);
AssertPublicApi(types[0].Assembly, types, expectedOutput, includeAssemblyAttributes, whitelistedNamespacePrefixes, excludeAttributes);
}

private static void AssertPublicApi(Assembly assembly, Type[] types, string expectedOutput, bool includeAssemblyAttributes, string[] whitelistedNamespacePrefixes, string[] excludeAttributes)
Expand Down