Skip to content

Commit

Permalink
Merge pull request #115 from sungam3r/Nullable
Browse files Browse the repository at this point in the history
C# 8 nullable reference type support
  • Loading branch information
danielmarbach committed Nov 2, 2019
2 parents ab10206 + c5db6cc commit 6c8d8d7
Show file tree
Hide file tree
Showing 17 changed files with 1,242 additions and 122 deletions.
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
{
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

0 comments on commit 6c8d8d7

Please sign in to comment.