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

[WIP] C# 8 nullable reference type support #102

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 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
7 changes: 5 additions & 2 deletions src/ApiApprover.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ 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
..\.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
Original file line number Diff line number Diff line change
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
2 changes: 1 addition & 1 deletion src/PublicApiGenerator.Tool/Program.cs
Original file line number Diff line number Diff line change
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 Down
177 changes: 81 additions & 96 deletions src/PublicApiGenerator/ApiGenerator.cs

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions src/PublicApiGenerator/CSharpAlias.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
139 changes: 139 additions & 0 deletions src/PublicApiGenerator/CodeTypeReferenceBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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 = 1000;

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

static CodeTypeReference CreateCodeTypeReferenceWithNullabilityMap(TypeReference type, IEnumerator<bool> nullabilityMap, bool forceNullable)
{
var typeName = GetTypeName(type, nullabilityMap, forceNullable);
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, true);
}
else
{
return new CodeTypeReference(typeName, CreateGenericArguments(type, nullabilityMap));
}
}

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(false, MAX_COUNT);

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

return Enumerable.Repeat((byte)value == 2, MAX_COUNT);
}

// 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, bool forceNullable)
{
bool nullable = forceNullable || HasAnyReferenceType(type) && IsNullable();

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

if (nullable)
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;
}
}

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

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

if (!type.IsNested)
{
return (!string.IsNullOrEmpty(type.Namespace) ? (type.Namespace + ".") : "") + type.Name;
}

return GetTypeName(type.DeclaringType, null, false) + "." + GetTypeName(type, nullabilityMap, false);
}

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, false));
}
return genericArguments.ToArray();
}
}
}
40 changes: 40 additions & 0 deletions src/PublicApiGenerator/NullableContext.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}
2 changes: 1 addition & 1 deletion src/PublicApiGenerator/PublicApiGenerator.csproj
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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);
danielmarbach marked this conversation as resolved.
Show resolved Hide resolved
}

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