diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..08252f2
--- /dev/null
+++ b/.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
diff --git a/README.md b/README.md
index 1e1d62f..62208b3 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/ApiApprover.sln b/src/ApiApprover.sln
index e1355fb..6c98439 100644
--- a/src/ApiApprover.sln
+++ b/src/ApiApprover.sln
@@ -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
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index fad5149..97295ef 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -7,6 +7,7 @@
Copyright Jake Ginnivan 2010-$([System.DateTime]::UtcNow.ToString(yyyy)). All rights reserved.
semanticversioning versioning api
..\..\nugets
+ latest
diff --git a/src/PublicApiGenerator.Tool/Program.cs b/src/PublicApiGenerator.Tool/Program.cs
index 9138467..0a44ccc 100644
--- a/src/PublicApiGenerator.Tool/Program.cs
+++ b/src/PublicApiGenerator.Tool/Program.cs
@@ -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);
}
@@ -259,4 +259,4 @@ public static void Main(string[] args)
}
";
}
-}
\ No newline at end of file
+}
diff --git a/src/PublicApiGenerator/ApiGenerator.cs b/src/PublicApiGenerator/ApiGenerator.cs
index e07e131..aaa2fb4 100644
--- a/src/PublicApiGenerator/ApiGenerator.cs
+++ b/src/PublicApiGenerator/ApiGenerator.cs
@@ -1,18 +1,18 @@
+using Microsoft.CSharp;
+using Mono.Cecil;
+using Mono.Cecil.Rocks;
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
-using Microsoft.CSharp;
-using Mono.Cecil;
-using Mono.Cecil.Rocks;
using ICustomAttributeProvider = Mono.Cecil.ICustomAttributeProvider;
using TypeAttributes = System.Reflection.TypeAttributes;
-using System.Globalization;
// ReSharper disable BitwiseOperatorOnEnumWithoutFlags
namespace PublicApiGenerator
@@ -54,6 +54,7 @@ static string RemoveUnnecessaryWhiteSpace(string publicApi)
Environment.NewLine
}, StringSplitOptions.RemoveEmptyEntries)
.Where(l => !string.IsNullOrWhiteSpace(l))
+ .Select(l => l.TrimEnd())
);
}
@@ -91,8 +92,11 @@ static string CreatePublicApiForAssembly(AssemblyDefinition assembly, Func excludeAttributes)
{
- if (memberInfo is MethodDefinition methodDefinition)
+ using (NullableContext.Push(memberInfo))
{
- if (methodDefinition.IsConstructor)
- AddCtorToTypeDeclaration(typeDeclaration, methodDefinition, excludeAttributes);
- else
- AddMethodToTypeDeclaration(typeDeclaration, methodDefinition, excludeAttributes);
- }
- else if (memberInfo is PropertyDefinition propertyDefinition)
- {
- AddPropertyToTypeDeclaration(typeDeclaration, propertyDefinition, excludeAttributes);
- }
- else if (memberInfo is EventDefinition eventDefinition)
- {
- typeDeclaration.Members.Add(GenerateEvent(eventDefinition, excludeAttributes));
- }
- else if (memberInfo is FieldDefinition fieldDefinition)
- {
- AddFieldToTypeDeclaration(typeDeclaration, fieldDefinition, excludeAttributes);
+ if (memberInfo is MethodDefinition methodDefinition)
+ {
+ if (methodDefinition.IsConstructor)
+ AddCtorToTypeDeclaration(typeDeclaration, methodDefinition, excludeAttributes);
+ else
+ AddMethodToTypeDeclaration(typeDeclaration, methodDefinition, excludeAttributes);
+ }
+ else if (memberInfo is PropertyDefinition propertyDefinition)
+ {
+ AddPropertyToTypeDeclaration(typeDeclaration, propertyDefinition, excludeAttributes);
+ }
+ else if (memberInfo is EventDefinition eventDefinition)
+ {
+ typeDeclaration.Members.Add(GenerateEvent(eventDefinition, excludeAttributes));
+ }
+ else if (memberInfo is FieldDefinition fieldDefinition)
+ {
+ AddFieldToTypeDeclaration(typeDeclaration, fieldDefinition, excludeAttributes);
+ }
}
}
@@ -210,7 +217,7 @@ static CodeTypeDeclaration CreateTypeDeclaration(TypeDefinition publicType, stri
var name = publicType.Name;
var isStruct = publicType.IsValueType && !publicType.IsPrimitive && !publicType.IsEnum;
-
+
var @readonly = isStruct && publicType.CustomAttributes.Any(a =>
a.AttributeType.FullName == "System.Runtime.CompilerServices.IsReadOnlyAttribute");
@@ -225,7 +232,7 @@ static CodeTypeDeclaration CreateTypeDeclaration(TypeDefinition publicType, stri
declarationName += "static ";
declarationName += name;
-
+
var declaration = new CodeTypeDeclaration(declarationName)
{
CustomAttributes = CreateCustomAttributes(publicType, excludeAttributes),
@@ -240,7 +247,20 @@ static CodeTypeDeclaration CreateTypeDeclaration(TypeDefinition publicType, stri
if (declaration.IsInterface && publicType.BaseType != null)
throw new NotImplementedException("Base types for interfaces needs testing");
- PopulateGenericParameters(publicType, declaration.TypeParameters, excludeAttributes);
+ PopulateGenericParameters(publicType, declaration.TypeParameters, excludeAttributes, parameter =>
+ {
+ var declaringType = publicType.DeclaringType;
+
+ while (declaringType != null)
+ {
+ if (declaringType.GenericParameters.Any(p => p.Name == parameter.Name))
+ return false; // https://github.com/ApiApprover/ApiApprover/issues/108
+
+ declaringType = declaringType.DeclaringType;
+ }
+
+ return true;
+ });
if (publicType.BaseType != null && ShouldOutputBaseType(publicType))
{
@@ -248,16 +268,16 @@ static CodeTypeDeclaration CreateTypeDeclaration(TypeDefinition publicType, stri
{
var underlyingType = publicType.GetEnumUnderlyingType();
if (underlyingType.FullName != "System.Int32")
- declaration.BaseTypes.Add(CreateCodeTypeReference(underlyingType));
+ declaration.BaseTypes.Add(underlyingType.CreateCodeTypeReference());
}
else
- declaration.BaseTypes.Add(CreateCodeTypeReference(publicType.BaseType));
+ declaration.BaseTypes.Add(publicType.BaseType.CreateCodeTypeReference(publicType));
}
- foreach(var @interface in publicType.Interfaces.OrderBy(i => i.InterfaceType.FullName, StringComparer.Ordinal)
+ 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))
.Select(t => t.Reference))
- declaration.BaseTypes.Add(CreateCodeTypeReference(@interface.InterfaceType));
+ 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, excludeAttributes);
@@ -271,8 +291,11 @@ static CodeTypeDeclaration CreateTypeDeclaration(TypeDefinition publicType, stri
foreach (var nestedType in publicType.NestedTypes.Where(ShouldIncludeType).OrderBy(t => t.FullName, StringComparer.Ordinal))
{
- var nestedTypeDeclaration = CreateTypeDeclaration(nestedType, whitelistedNamespacePrefixes, excludeAttributes);
- declaration.Members.Add(nestedTypeDeclaration);
+ using (NullableContext.Push(nestedType))
+ {
+ var nestedTypeDeclaration = CreateTypeDeclaration(nestedType, whitelistedNamespacePrefixes, excludeAttributes);
+ declaration.Members.Add(nestedTypeDeclaration);
+ }
}
return declaration;
@@ -281,31 +304,34 @@ static CodeTypeDeclaration CreateTypeDeclaration(TypeDefinition publicType, stri
static CodeTypeDeclaration CreateDelegateDeclaration(TypeDefinition publicType, HashSet excludeAttributes)
{
var invokeMethod = publicType.Methods.Single(m => m.Name == "Invoke");
- var name = publicType.Name;
- var index = name.IndexOf('`');
- if (index != -1)
- name = name.Substring(0, index);
- var declaration = new CodeTypeDelegate(name)
+ using (NullableContext.Push(invokeMethod)) // for delegates NullableContextAttribute is stored on Invoke method
{
- Attributes = MemberAttributes.Public,
- CustomAttributes = CreateCustomAttributes(publicType, excludeAttributes),
- ReturnType = CreateCodeTypeReference(invokeMethod.ReturnType),
- };
+ var name = publicType.Name;
+ var index = name.IndexOf('`');
+ if (index != -1)
+ name = name.Substring(0, index);
+ var declaration = new CodeTypeDelegate(name)
+ {
+ Attributes = MemberAttributes.Public,
+ CustomAttributes = CreateCustomAttributes(publicType, excludeAttributes),
+ ReturnType = invokeMethod.ReturnType.CreateCodeTypeReference(invokeMethod.MethodReturnType),
+ };
- // CodeDOM. No support. Return type attributes.
- PopulateCustomAttributes(invokeMethod.MethodReturnType, declaration.CustomAttributes, type => ModifyCodeTypeReference(type, "return:"), excludeAttributes);
- PopulateGenericParameters(publicType, declaration.TypeParameters, excludeAttributes);
- PopulateMethodParameters(invokeMethod, declaration.Parameters, excludeAttributes);
+ // CodeDOM. No support. Return type attributes.
+ PopulateCustomAttributes(invokeMethod.MethodReturnType, declaration.CustomAttributes, type => ModifyCodeTypeReference(type, "return:"), excludeAttributes);
+ PopulateGenericParameters(publicType, declaration.TypeParameters, excludeAttributes, _ => true);
+ PopulateMethodParameters(invokeMethod, declaration.Parameters, excludeAttributes);
- // Of course, CodeDOM doesn't support generic type parameters for delegates. Of course.
- if (declaration.TypeParameters.Count > 0)
- {
- var parameterNames = from parameterType in declaration.TypeParameters.Cast()
- select parameterType.Name;
- declaration.Name = string.Format(CultureInfo.InvariantCulture, "{0}<{1}>", declaration.Name, string.Join(", ", parameterNames));
- }
+ // Of course, CodeDOM doesn't support generic type parameters for delegates. Of course.
+ if (declaration.TypeParameters.Count > 0)
+ {
+ var parameterNames = from parameterType in declaration.TypeParameters.Cast()
+ select parameterType.Name;
+ declaration.Name = string.Format(CultureInfo.InvariantCulture, "{0}<{1}>", declaration.Name, string.Join(", ", parameterNames));
+ }
- return declaration;
+ return declaration;
+ }
}
static bool ShouldOutputBaseType(TypeDefinition publicType)
@@ -313,9 +339,9 @@ static bool ShouldOutputBaseType(TypeDefinition publicType)
return publicType.BaseType.FullName != "System.Object" && publicType.BaseType.FullName != "System.ValueType";
}
- static void PopulateGenericParameters(IGenericParameterProvider publicType, CodeTypeParameterCollection parameters, HashSet excludeAttributes)
+ static void PopulateGenericParameters(IGenericParameterProvider publicType, CodeTypeParameterCollection parameters, HashSet excludeAttributes, Func shouldUseParameter)
{
- foreach (var parameter in publicType.GenericParameters)
+ foreach (var parameter in publicType.GenericParameters.Where(shouldUseParameter))
{
// A little hacky. Means we get "in" and "out" prefixed on any constraints, but it's either that
// or add it as a custom attribute
@@ -339,17 +365,41 @@ static void PopulateGenericParameters(IGenericParameterProvider publicType, Code
typeParameter.CustomAttributes.AddRange(attributeCollection.OfType().ToArray());
+ var nullableConstraint = parameter.GetNullabilityMap().First();
+ var unmanagedConstraint = parameter.CustomAttributes.Any(attr => attr.AttributeType.FullName == "System.Runtime.CompilerServices.IsUnmanagedAttribute");
+
if (parameter.HasNotNullableValueTypeConstraint)
- typeParameter.Constraints.Add(" struct"); // Extra space is a hack!
+ typeParameter.Constraints.Add(unmanagedConstraint ? " unmanaged" : " struct");
+
if (parameter.HasReferenceTypeConstraint)
- typeParameter.Constraints.Add(" class");
- foreach (var constraint in parameter.Constraints.Where(t => t.FullName != "System.ValueType"))
+ typeParameter.Constraints.Add(nullableConstraint == true ? " class?" : " class");
+ else if (nullableConstraint == false)
+ typeParameter.Constraints.Add(" notnull");
+
+ using (NullableContext.Push(parameter))
{
- // for generic constraints like IEnumerable call to GetElementType() returns TypeReference with Name = !0
- typeParameter.Constraints.Add(CreateCodeTypeReference(constraint/*.GetElementType()*/));
+ foreach (var constraint in parameter.Constraints.Where(constraint => !IsSpecialConstraint(constraint)))
+ {
+ // for generic constraints like IEnumerable call to GetElementType() returns TypeReference with Name = !0
+ var typeReference = constraint.ConstraintType /*.GetElementType()*/.CreateCodeTypeReference(constraint);
+ typeParameter.Constraints.Add(typeReference);
+ }
}
parameters.Add(typeParameter);
}
+
+ bool IsSpecialConstraint(GenericParameterConstraint constraint)
+ {
+ // struct
+ if (constraint.ConstraintType is TypeReference reference && reference.FullName == "System.ValueType")
+ return true;
+
+ // unmanaged
+ if (constraint.ConstraintType is RequiredModifierType type && type.ModifierType.FullName == "System.Runtime.InteropServices.UnmanagedType")
+ return true;
+
+ return false;
+ }
}
static CodeAttributeDeclarationCollection CreateCustomAttributes(ICustomAttributeProvider type,
@@ -381,7 +431,7 @@ static void PopulateGenericParameters(IGenericParameterProvider publicType, Code
static CodeAttributeDeclaration GenerateCodeAttributeDeclaration(Func codeTypeModifier, CustomAttribute customAttribute)
{
- var attribute = new CodeAttributeDeclaration(codeTypeModifier(CreateCodeTypeReference(customAttribute.AttributeType)));
+ var attribute = new CodeAttributeDeclaration(codeTypeModifier(customAttribute.AttributeType.CreateCodeTypeReference(mode: NullableMode.Disable)));
foreach (var arg in customAttribute.ConstructorArguments)
{
attribute.Arguments.Add(new CodeAttributeArgument(CreateInitialiserExpression(arg)));
@@ -432,6 +482,10 @@ static string ConvertAttributeToCode(Func
"System.Runtime.CompilerServices.RuntimeCompatibilityAttribute",
"System.Runtime.CompilerServices.IteratorStateMachineAttribute",
"System.Runtime.CompilerServices.IsReadOnlyAttribute",
+ "System.Runtime.CompilerServices.NullableAttribute",
+ "System.Runtime.CompilerServices.NullableContextAttribute",
+ "System.Runtime.CompilerServices.IsUnmanagedAttribute",
+ //"System.Runtime.CompilerServices.DynamicAttribute",
"System.Reflection.DefaultMemberAttribute",
"System.Diagnostics.DebuggableAttribute",
"System.Diagnostics.DebuggerNonUserCodeAttribute",
@@ -464,7 +518,7 @@ static CodeExpression CreateInitialiserExpression(CustomAttributeArgument attrib
{
var initialisers = from argument in customAttributeArguments
select CreateInitialiserExpression(argument);
- return new CodeArrayCreateExpression(CreateCodeTypeReference(attributeArgument.Type), initialisers.ToArray());
+ return new CodeArrayCreateExpression(attributeArgument.Type.CreateCodeTypeReference(), initialisers.ToArray());
}
var type = attributeArgument.Type.Resolve();
@@ -495,13 +549,13 @@ static CodeExpression CreateInitialiserExpression(CustomAttributeArgument attrib
where f.Constant != null
let v = Convert.ToInt64(f.Constant)
where v == originalValue
- select new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(CreateCodeTypeReference(type)), f.Name);
+ select new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(type.CreateCodeTypeReference()), f.Name);
return allFlags.FirstOrDefault();
}
if (type.FullName == "System.Type" && value is TypeReference typeRef)
{
- return new CodeTypeOfExpression(CreateCodeTypeReference(typeRef));
+ return new CodeTypeOfExpression(typeRef.CreateCodeTypeReference());
}
if (value is string)
@@ -570,7 +624,7 @@ static void AddMethodToTypeDeclaration(CodeTypeDeclaration typeDeclaration, Meth
memberName = mappedMemberName;
}
- var returnType = CreateCodeTypeReference(member.ReturnType);
+ var returnType = member.ReturnType.CreateCodeTypeReference(member.MethodReturnType);
var method = new CodeMemberMethod
{
@@ -580,7 +634,7 @@ static void AddMethodToTypeDeclaration(CodeTypeDeclaration typeDeclaration, Meth
ReturnType = returnType,
};
PopulateCustomAttributes(member.MethodReturnType, method.ReturnTypeCustomAttributes, excludeAttributes);
- PopulateGenericParameters(member, method.TypeParameters, excludeAttributes);
+ PopulateGenericParameters(member, method.TypeParameters, excludeAttributes, _ => true);
PopulateMethodParameters(member, method.Parameters, excludeAttributes, IsExtensionMethod(member));
typeDeclaration.Members.Add(method);
@@ -615,7 +669,7 @@ static bool IsExtensionMethod(ICustomAttributeProvider method)
parameterType = byReferenceType.ElementType;
}
- var type = CreateCodeTypeReference(parameterType);
+ var type = parameterType.CreateCodeTypeReference(parameter);
if (isExtension)
{
@@ -691,9 +745,9 @@ static bool IsHidingMethod(MethodDefinition method)
if (typeDefinition.IsInterface)
{
var interfaceMethods = from @interfaceReference in typeDefinition.Interfaces
- let interfaceDefinition = @interfaceReference.InterfaceType.Resolve()
- where interfaceDefinition != null
- select interfaceDefinition.Methods;
+ let interfaceDefinition = @interfaceReference.InterfaceType.Resolve()
+ where interfaceDefinition != null
+ select interfaceDefinition.Methods;
return interfaceMethods.Any(ms => MetadataResolver.GetMethod(ms, method) != null);
}
@@ -728,7 +782,7 @@ static void AddPropertyToTypeDeclaration(CodeTypeDeclaration typeDeclaration, Pr
var propertyType = member.PropertyType.IsGenericParameter
? new CodeTypeReference(member.PropertyType.Name)
- : CreateCodeTypeReference(member.PropertyType);
+ : member.PropertyType.CreateCodeTypeReference(member);
var property = new CodeMemberProperty
{
@@ -754,7 +808,7 @@ static void AddPropertyToTypeDeclaration(CodeTypeDeclaration typeDeclaration, Pr
foreach (var parameter in member.Parameters)
{
property.Parameters.Add(
- new CodeParameterDeclarationExpression(CreateCodeTypeReference(parameter.ParameterType),
+ new CodeParameterDeclarationExpression(parameter.ParameterType.CreateCodeTypeReference(parameter),
parameter.Name));
}
@@ -785,12 +839,12 @@ static MemberAttributes GetPropertyAttributes(MemberAttributes getterAttributes,
// Scope should be the same for getter and setter. If one isn't specified, it'll be 0
var getterScope = getterAttributes & MemberAttributes.ScopeMask;
var setterScope = setterAttributes & MemberAttributes.ScopeMask;
- var scope = (MemberAttributes) Math.Max((int) getterScope, (int) setterScope);
+ var scope = (MemberAttributes)Math.Max((int)getterScope, (int)setterScope);
// Vtable should be the same for getter and setter. If one isn't specified, it'll be 0
var getterVtable = getterAttributes & MemberAttributes.VTableMask;
var setterVtable = setterAttributes & MemberAttributes.VTableMask;
- var vtable = (MemberAttributes) Math.Max((int) getterVtable, (int) setterVtable);
+ var vtable = (MemberAttributes)Math.Max((int)getterVtable, (int)setterVtable);
return access | scope | vtable;
}
@@ -810,7 +864,7 @@ static CodeTypeMember GenerateEvent(EventDefinition eventDefinition, HashSet().ToArray());
}
}
-
- static CodeTypeReference CreateCodeTypeReference(TypeReference type)
- {
- var typeName = GetTypeName(type);
- return new CodeTypeReference(typeName, CreateGenericArguments(type));
- }
-
- static string GetTypeName(TypeReference type)
- {
- if (type.IsGenericParameter)
- return type.Name;
-
- if (!type.IsNested)
- {
- return (!string.IsNullOrEmpty(type.Namespace) ? (type.Namespace + ".") : "") + type.Name;
- }
-
- return GetTypeName(type.DeclaringType) + "." + type.Name;
- }
-
- static CodeTypeReference[] CreateGenericArguments(TypeReference type)
- {
- // ReSharper disable once RedundantEnumerableCastCall
- var genericArgs = type is IGenericInstance instance ? instance.GenericArguments : type.HasGenericParameters ? type.GenericParameters.Cast() : null;
- if (genericArgs == null) return null;
-
- var genericArguments = new List();
- foreach (var argument in genericArgs)
- {
- genericArguments.Add(CreateCodeTypeReference(argument));
- }
- return genericArguments.ToArray();
- }
}
static class CecilEx
diff --git a/src/PublicApiGenerator/CSharpAlias.cs b/src/PublicApiGenerator/CSharpAlias.cs
new file mode 100644
index 0000000..3923226
--- /dev/null
+++ b/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;
+ }
+ }
+ }
+}
diff --git a/src/PublicApiGenerator/CodeTypeReferenceBuilder.cs b/src/PublicApiGenerator/CodeTypeReferenceBuilder.cs
new file mode 100644
index 0000000..e3418f3
--- /dev/null
+++ b/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 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 into Type? for readability
+ var genericArgs = type is IGenericInstance instance ? instance.GenericArguments : type.HasGenericParameters ? type.GenericParameters.Cast() : null;
+ return CreateCodeTypeReferenceWithNullabilityMap(genericArgs.Single(), nullabilityMap, NullableMode.Force, disableNested);
+ }
+ else
+ {
+ return new CodeTypeReference(typeName, CreateGenericArguments(type, nullabilityMap));
+ }
+ }
+
+ static CodeTypeReference[] CreateGenericArguments(TypeReference type, IEnumerator nullabilityMap)
+ {
+ // ReSharper disable once RedundantEnumerableCastCall
+ var genericArgs = type is IGenericInstance instance ? instance.GenericArguments : type.HasGenericParameters ? type.GenericParameters.Cast() : null;
+ if (genericArgs == null) return null;
+
+ var genericArguments = new List();
+ foreach (var argument in genericArgs)
+ {
+ genericArguments.Add(CreateCodeTypeReferenceWithNullabilityMap(argument, nullabilityMap, NullableMode.Default, false));
+ }
+ return genericArguments.ToArray();
+ }
+
+ internal static IEnumerable 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, 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() : null;
+ if (genericArgs == null) return false;
+
+ foreach (var argument in genericArgs)
+ {
+ if (HasAnyReferenceType(argument))
+ return true;
+ }
+
+ return false;
+ }
+
+ static string GetTypeName(TypeReference type, IEnumerator 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 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);
+ }
+ }
+}
diff --git a/src/PublicApiGenerator/NullableContext.cs b/src/PublicApiGenerator/NullableContext.cs
new file mode 100644
index 0000000..8447fc0
--- /dev/null
+++ b/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 _nullableContextProviders;
+
+ private static Stack NullableContextProviders
+ {
+ get
+ {
+ if (_nullableContextProviders == null)
+ _nullableContextProviders = new Stack();
+ return _nullableContextProviders;
+ }
+ }
+
+ internal static IDisposable Push(ICustomAttributeProvider provider) => new PopDisposable(provider);
+
+ internal static IEnumerable Providers => NullableContextProviders;
+
+ private sealed class PopDisposable : IDisposable
+ {
+ public PopDisposable(ICustomAttributeProvider provider)
+ {
+ NullableContextProviders.Push(provider);
+ }
+
+ public void Dispose()
+ {
+ NullableContextProviders.Pop();
+ }
+ }
+ }
+}
diff --git a/src/PublicApiGenerator/NullableMode.cs b/src/PublicApiGenerator/NullableMode.cs
new file mode 100644
index 0000000..ab5485e
--- /dev/null
+++ b/src/PublicApiGenerator/NullableMode.cs
@@ -0,0 +1,9 @@
+namespace PublicApiGenerator
+{
+ enum NullableMode
+ {
+ Default,
+ Force,
+ Disable
+ }
+}
diff --git a/src/PublicApiGenerator/PublicApiGenerator.csproj b/src/PublicApiGenerator/PublicApiGenerator.csproj
index 34454c1..6c96007 100644
--- a/src/PublicApiGenerator/PublicApiGenerator.csproj
+++ b/src/PublicApiGenerator/PublicApiGenerator.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/src/PublicApiGeneratorTests/ApiGeneratorTestsBase.cs b/src/PublicApiGeneratorTests/ApiGeneratorTestsBase.cs
index 0637934..53b5ca7 100644
--- a/src/PublicApiGeneratorTests/ApiGeneratorTestsBase.cs
+++ b/src/PublicApiGeneratorTests/ApiGeneratorTestsBase.cs
@@ -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)
diff --git a/src/PublicApiGeneratorTests/Class_nested.cs b/src/PublicApiGeneratorTests/Class_nested.cs
index 2aadf11..ab53f9b 100644
--- a/src/PublicApiGeneratorTests/Class_nested.cs
+++ b/src/PublicApiGeneratorTests/Class_nested.cs
@@ -1,4 +1,5 @@
-using PublicApiGeneratorTests.Examples;
+using PublicApiGeneratorTests.Examples;
+using System.Collections.Generic;
using Xunit;
namespace PublicApiGeneratorTests
@@ -122,6 +123,71 @@ public struct InnerNestedStruct
}
}
}
+}");
+ }
+
+ [Fact]
+ public void Should_output_Nested_Classes_From_NullableExample1()
+ {
+ AssertPublicApi(typeof(Foo),
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class Foo
+ {
+ public Foo(PublicApiGeneratorTests.Examples.Foo.Bar bar) { }
+ public class Bar
+ {
+ public Bar(PublicApiGeneratorTests.Examples.Foo.Bar.Baz? baz) { }
+ public class Baz
+ {
+ public Baz() { }
+ }
+ }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_output_Nested_Classes_From_NullableExample2()
+ {
+ AssertPublicApi(typeof(Foo<>),
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class Foo
+ {
+ public Foo(PublicApiGeneratorTests.Examples.Foo.Bar bar) { }
+ public class Bar
+ {
+ public Bar(PublicApiGeneratorTests.Examples.Foo.Bar.Baz? baz) { }
+ public class Baz
+ {
+ public Baz() { }
+ }
+ }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Not_Output_Generic_Parameters_From_Declaring_Type()
+ {
+ AssertPublicApi(typeof(Foo<,>),
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class Foo
+ {
+ public Foo() { }
+ public class Bar
+ {
+ public T1 Data;
+ public Bar() { }
+ }
+ public class Bar
+ {
+ public System.Collections.Generic.List? Field;
+ public Bar() { }
+ }
+ }
}");
}
}
@@ -205,8 +271,54 @@ public struct NestedStruct
public void Method() { }
}
+
+#nullable enable
+
+ // nullable example
+ public class Foo
+ {
+ public class Bar
+ {
+ public class Baz { }
+
+ public Bar(Baz? baz) { }
+ }
+
+ public Foo(Bar bar)
+ {
+ }
+ }
+
+ // nullable generic example 1
+ public class Foo
+ {
+ public class Bar
+ {
+ public class Baz { }
+
+ public Bar(Baz? baz) { }
+ }
+
+ public Foo(Bar bar)
+ {
+ }
+ }
+
+ // nullable generic example 2
+ public class Foo
+ {
+ public class Bar
+ {
+ public T1 Data;
+ }
+
+ public class Bar
+ {
+ public List? Field;
+ }
+ }
}
// ReSharper restore UnusedMember.Local
// ReSharper restore ClassNeverInstantiated.Global
// ReSharper restore UnusedMember.Global
-}
\ No newline at end of file
+}
diff --git a/src/PublicApiGeneratorTests/Dynamics.cs b/src/PublicApiGeneratorTests/Dynamics.cs
new file mode 100644
index 0000000..c240c19
--- /dev/null
+++ b/src/PublicApiGeneratorTests/Dynamics.cs
@@ -0,0 +1,44 @@
+using PublicApiGeneratorTests.Examples;
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace PublicApiGeneratorTests
+{
+ public class Dynamics : ApiGeneratorTestsBase
+ {
+ [Fact(Skip = "Needs investigation")]
+ public void Should_output_dynamic()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class ClassWithDynamic : System.Collections.Generic.List
+ {
+ public class ClassWithDynamic2 : System.Collections.Generic.List { }
+ public ClassWithDynamic() { }
+ public dynamic DoIt1(dynamic p) { }
+ public dynamic? DoIt2(dynamic? p) { }
+ }
+}");
+ }
+
+ // TODO: Enum with flags + undefined value
+ // Not supported by Cecil?
+ }
+
+ // ReSharper disable UnusedMember.Global
+ namespace Examples
+ {
+#nullable enable
+ public class ClassWithDynamic : List
+ {
+ public class ClassWithDynamic2 : List { }
+
+ public dynamic DoIt1(dynamic p) { throw null; }
+
+ public dynamic? DoIt2(dynamic? p) { throw null; }
+ }
+ }
+ // ReSharper restore UnusedMember.Global
+}
diff --git a/src/PublicApiGeneratorTests/Field_modifiers.cs b/src/PublicApiGeneratorTests/Field_modifiers.cs
index c5530aa..b150836 100644
--- a/src/PublicApiGeneratorTests/Field_modifiers.cs
+++ b/src/PublicApiGeneratorTests/Field_modifiers.cs
@@ -1,4 +1,4 @@
-using PublicApiGeneratorTests.Examples;
+using PublicApiGeneratorTests.Examples;
using Xunit;
namespace PublicApiGeneratorTests
@@ -20,6 +20,20 @@ public class ClassWithStaticFields
}");
}
+ [Fact]
+ public void Include_Volatile_field_Without_modreq()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class ClassWithVolatileField
+ {
+ public static int StaticVolatilePublicField;
+ public ClassWithVolatileField() { }
+ }
+}");
+ }
+
[Fact]
public void Include_readonly_fields_without_constant_values()
{
@@ -64,6 +78,11 @@ public class ClassWithStaticFields
protected static string StaticProtectedField;
}
+ public class ClassWithVolatileField
+ {
+ public static volatile int StaticVolatilePublicField;
+ }
+
public class ClassWithReadonlyFields
{
public readonly int ReadonlyPublicField = 42;
@@ -78,4 +97,4 @@ public class ClassWithConstFields
}
// ReSharper restore UnusedMember.Global
// ReSharper restore ClassNeverInstantiated.Global
-}
\ No newline at end of file
+}
diff --git a/src/PublicApiGeneratorTests/NullableTests.cs b/src/PublicApiGeneratorTests/NullableTests.cs
new file mode 100644
index 0000000..5f56f08
--- /dev/null
+++ b/src/PublicApiGeneratorTests/NullableTests.cs
@@ -0,0 +1,650 @@
+using PublicApiGeneratorTests.Examples;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Xunit;
+
+namespace PublicApiGeneratorTests
+{
+ // Tests for https://github.com/ApiApprover/ApiApprover/issues/54
+ // See also https://github.com/dotnet/roslyn/blob/master/docs/features/nullable-reference-types.md
+ [Trait("NRT", "Nullable Reference Types")]
+ public class NullableTests : ApiGeneratorTestsBase
+ {
+ [Fact]
+ public void Should_Annotate_ReturnType()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class ReturnType
+ {
+ public ReturnType() { }
+ public string? ReturnProperty { get; set; }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_VoidReturn()
+ {
+ AssertPublicApi(typeof(VoidReturn),
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class static VoidReturn
+ {
+ public static void ShouldBeEquivalentTo(this object? actual, object? expected) { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Derived_ReturnType()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class ReturnArgs : System.EventArgs
+ {
+ public ReturnArgs() { }
+ public string? Target { get; set; }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Ctor_Args()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class NullableCtor
+ {
+ public NullableCtor(string? nullableLabel, string nope) { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Not_Annotate_Obsolete_Attribute()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ [System.ObsoleteAttribute(""Foo"")]
+ public class ClassWithObsolete
+ {
+ [System.ObsoleteAttribute(""Bar"")]
+ public ClassWithObsolete(string? nullableLabel) { }
+ [System.ObsoleteAttribute(""Bar"")]
+ public ClassWithObsolete(string? nullableLabel, string? nullableLabel2) { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Generic_Event()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class GenericEvent
+ {
+ public GenericEvent() { }
+ public event System.EventHandler ReturnEvent;
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Delegate_Declaration()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class DelegateDeclaration
+ {
+ public DelegateDeclaration() { }
+ public delegate string? OnNullableReturn(object sender, PublicApiGeneratorTests.Examples.ReturnArgs? args);
+ public delegate string OnReturn(object sender, PublicApiGeneratorTests.Examples.ReturnArgs? args);
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Nullable_Array()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class NullableArray
+ {
+ public NullableArray() { }
+ public PublicApiGeneratorTests.Examples.ReturnType[]? NullableMethod1() { }
+ public PublicApiGeneratorTests.Examples.ReturnType[]?[]? NullableMethod2() { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Nullable_Enumerable()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class NullableEnumerable
+ {
+ public NullableEnumerable() { }
+ public System.Collections.Generic.IEnumerable? Enumerable() { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Generic_Method()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class GenericMethod
+ {
+ public GenericMethod() { }
+ public PublicApiGeneratorTests.Examples.ReturnType? NullableGenericMethod(T1? t1, T2 t2, T3? t3)
+ where T1 : class
+ where T2 : class
+ where T3 : class { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Skeet_Examples()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class SkeetExamplesClass
+ {
+ public System.Collections.Generic.Dictionary, string[]?> SkeetExample;
+ public System.Collections.Generic.Dictionary, string?[]> SkeetExample2;
+ public System.Collections.Generic.Dictionary, string?[]?> SkeetExample3;
+ public SkeetExamplesClass() { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_By_Ref()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class ByRefClass
+ {
+ public ByRefClass() { }
+ public bool ByRefNullableReferenceParam(PublicApiGeneratorTests.Examples.ReturnType rt1, ref PublicApiGeneratorTests.Examples.ReturnType? rt2, PublicApiGeneratorTests.Examples.ReturnType rt3, PublicApiGeneratorTests.Examples.ReturnType? rt4, out PublicApiGeneratorTests.Examples.ReturnType? rt5, PublicApiGeneratorTests.Examples.ReturnType rt6) { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Different_API()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class NullableApi
+ {
+ public PublicApiGeneratorTests.Examples.ReturnType NonNullField;
+ public PublicApiGeneratorTests.Examples.ReturnType? NullableField;
+ public NullableApi() { }
+ public System.Collections.Generic.Dictionary?>>? ComplicatedDictionary { get; set; }
+ public PublicApiGeneratorTests.Examples.ReturnType NonNullProperty { get; set; }
+ public PublicApiGeneratorTests.Examples.ReturnType? NullableProperty { get; set; }
+ public string? Convert(string source) { }
+ public override bool Equals(object? obj) { }
+ public override int GetHashCode() { }
+ public PublicApiGeneratorTests.Examples.ReturnType? NullableParamAndReturnMethod(string? nullableParam, string nonNullParam, int? nullableValueType) { }
+ public PublicApiGeneratorTests.Examples.ReturnType NullableParamMethod(string? nullableParam, string nonNullParam, int? nullableValueType) { }
+ public PublicApiGeneratorTests.Examples.Data NullableStruct1(PublicApiGeneratorTests.Examples.Data param) { }
+ public PublicApiGeneratorTests.Examples.Data? NullableStruct2(PublicApiGeneratorTests.Examples.Data? param) { }
+ public PublicApiGeneratorTests.Examples.Data> NullableStruct3(PublicApiGeneratorTests.Examples.Data> param) { }
+ public PublicApiGeneratorTests.Examples.Data?> NullableStruct4(PublicApiGeneratorTests.Examples.Data?> param) { }
+ public PublicApiGeneratorTests.Examples.Data?>? NullableStruct5(PublicApiGeneratorTests.Examples.Data?>? param) { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_System_Nullable()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class SystemNullable
+ {
+ public readonly int? Age;
+ public SystemNullable() { }
+ public System.DateTime? Birth { get; set; }
+ public float? Calc(double? first, decimal? second) { }
+ public System.Collections.Generic.List GetSecrets(System.Collections.Generic.Dictionary> data) { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Generics()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class Generics
+ {
+ public Generics() { }
+ public System.Collections.Generic.List GetSecretData0() { }
+ public System.Collections.Generic.Dictionary> GetSecretData1() { }
+ public System.Collections.Generic.Dictionary?> GetSecretData2() { }
+ public System.Collections.Generic.Dictionary?>>> GetSecretData3(System.Collections.Generic.Dictionary>>>? value) { }
+ public System.Collections.Generic.Dictionary? GetSecretData4(System.Collections.Generic.Dictionary? value) { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Structs()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class Structs
+ {
+ public System.Collections.Generic.KeyValuePair field;
+ public Structs() { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Tuples()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class Tuples
+ {
+ public Tuples() { }
+ public System.Tuple Tuple1(System.Tuple? tuple) { }
+ public System.ValueTuple Tuple2(System.ValueTuple? tuple) { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Constraints()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class Constraints
+ {
+ public Constraints() { }
+ public void Print1(T val)
+ where T : class { }
+ public void Print2(T val)
+ where T : class? { }
+ public static void Print3()
+ where T : System.IO.Stream { }
+ public static void Print4()
+ where T : System.IDisposable { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Nullable_Constraints()
+ {
+ AssertPublicApi(typeof(Constraints2<,>),
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class Constraints2
+ where X : System.IComparable
+ where Y : class?
+ {
+ public Constraints2() { }
+ public T Convert(T data)
+ where T : System.IComparable { }
+ public static void Print1()
+ where T : System.IO.Stream? { }
+ public static void Print2()
+ where T : System.IDisposable? { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_BaseType()
+ {
+ AssertPublicApi(
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class NullableComparable : System.Collections.Generic.List, System.IComparable
+ {
+ public NullableComparable() { }
+ public int CompareTo(string? other) { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_OpenGeneric()
+ {
+ AssertPublicApi(typeof(StringNullableList<,>),
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public class StringNullableList : System.Collections.Generic.List, System.IComparable
+ where T : struct
+ where U : class
+ {
+ public StringNullableList() { }
+ public int CompareTo(U other) { }
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_NotNull_Constraint()
+ {
+ AssertPublicApi(typeof(IDoStuff1<,>),
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public interface IDoStuff1
+ where TIn : notnull
+ where TOut : notnull
+ {
+ TOut DoStuff(TIn input);
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_NotNull_And_Null_Constraint()
+ {
+ AssertPublicApi(typeof(IDoStuff2<,>),
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public interface IDoStuff2
+ where TIn : class?
+ where TOut : notnull
+ {
+ TOut DoStuff(TIn input);
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Without_Explicit_Constraints()
+ {
+ AssertPublicApi(typeof(IDoStuff3<,>),
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public interface IDoStuff3
+ {
+ TOut DoStuff(TIn input);
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_NotNull_And_Null_Class_Constraint()
+ {
+ AssertPublicApi(typeof(IDoStuff4<,>),
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public interface IDoStuff4
+ where TIn : class?
+ where TOut : class
+ {
+ TOut DoStuff(TIn input);
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Nullable_Class_And_Struct_Constraint()
+ {
+ AssertPublicApi(typeof(IDoStuff5<,>),
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public interface IDoStuff5
+ where TIn : class?
+ where TOut : struct
+ {
+ TOut DoStuff(TIn input);
+ }
+}");
+ }
+
+ [Fact]
+ public void Should_Annotate_Unmanaged_Constraint()
+ {
+ AssertPublicApi(typeof(IDoStuff6<,>),
+@"namespace PublicApiGeneratorTests.Examples
+{
+ public interface IDoStuff6
+ where TIn : notnull
+ where TOut : unmanaged
+ {
+ TOut DoStuff(TIn input);
+ }
+}");
+ }
+ }
+
+#nullable enable
+
+ // ReSharper disable ClassNeverInstantiated.Global
+ namespace Examples
+ {
+ public class ReturnType
+ {
+ public string? ReturnProperty { get; set; }
+ }
+
+ public static class VoidReturn
+ {
+ public static void ShouldBeEquivalentTo(this object? actual, object? expected) { }
+ }
+
+ public class ReturnArgs : EventArgs
+ {
+ public string? Target { get; set; }
+ }
+
+ public class NullableCtor
+ {
+ public NullableCtor(string? nullableLabel, string nope) { }
+ }
+
+ [Obsolete("Foo")]
+ public class ClassWithObsolete
+ {
+ [Obsolete("Bar")]
+ public ClassWithObsolete(string? nullableLabel) { }
+
+ [Obsolete("Bar")]
+ public ClassWithObsolete(string? nullableLabel, string? nullableLabel2) { }
+ }
+
+ public class GenericEvent
+ {
+ public event EventHandler ReturnEvent { add { } remove { } }
+ }
+
+ public class DelegateDeclaration
+ {
+ protected delegate string OnReturn(object sender, ReturnArgs? args);
+ protected delegate string? OnNullableReturn(object sender, ReturnArgs? args);
+ }
+
+ public class Structs
+ {
+ public KeyValuePair field;
+ }
+
+ public struct Data
+ {
+ public T Value { get; }
+ }
+
+ public class Generics
+ {
+ public List GetSecretData0() => null;
+ public Dictionary> GetSecretData1() => null;
+ public Dictionary?> GetSecretData2() => null;
+ public Dictionary?>>> GetSecretData3(Dictionary>>>? value) { return null; }
+ public Dictionary? GetSecretData4(Dictionary? value) { return null; }
+ }
+
+ public class NullableArray
+ {
+ public ReturnType[]? NullableMethod1() { return null; }
+ public ReturnType[]?[]? NullableMethod2() { return null; }
+ }
+
+ public class NullableEnumerable
+ {
+ public IEnumerable? Enumerable() { return null; }
+ }
+
+ public class GenericMethod
+ {
+ public ReturnType? NullableGenericMethod(T1? t1, T2 t2, T3? t3) where T1 : class where T2 : class where T3 : class { return null; }
+ }
+
+ public class SkeetExamplesClass
+ {
+ public Dictionary, string[]?> SkeetExample = new Dictionary, string[]?>();
+ public Dictionary, string?[]> SkeetExample2 = new Dictionary, string?[]>();
+ public Dictionary, string?[]?> SkeetExample3 = new Dictionary, string?[]?>();
+ }
+
+ public class ByRefClass
+ {
+ public bool ByRefNullableReferenceParam(ReturnType rt1, ref ReturnType? rt2, ReturnType rt3, ReturnType? rt4, out ReturnType? rt5, ReturnType rt6) { rt5 = null; return false; }
+ }
+
+ public class NullableApi
+ {
+ public ReturnType NonNullField = new ReturnType();
+ public ReturnType? NullableField;
+ public ReturnType NonNullProperty { get; protected set; } = new ReturnType();
+ public ReturnType? NullableProperty { get; set; }
+ public ReturnType NullableParamMethod(string? nullableParam, string nonNullParam, int? nullableValueType) { return new ReturnType(); }
+ public ReturnType? NullableParamAndReturnMethod(string? nullableParam, string nonNullParam, int? nullableValueType) { return default; }
+ public Dictionary?>>? ComplicatedDictionary { get; set; }
+ public override bool Equals(object? obj) => base.Equals(obj);
+ public override int GetHashCode() => base.GetHashCode();
+ public string? Convert(string source) => source;
+ public Data NullableStruct1(Data param) => default;
+ public Data? NullableStruct2(Data? param) => default;
+ public Data> NullableStruct3(Data> param) => default;
+ public Data?> NullableStruct4(Data?> param) => default;
+ public Data?>? NullableStruct5(Data?>? param) => default;
+ }
+
+ public class SystemNullable
+ {
+ public readonly int? Age;
+ public DateTime? Birth { get; set; }
+
+ public float? Calc(double? first, decimal? second) { return null; }
+
+ public List GetSecrets(Dictionary> data) => null;
+ }
+
+ public class Tuples
+ {
+ public Tuple Tuple1(Tuple? tuple) => default;
+ public ValueTuple Tuple2(ValueTuple? tuple) => default;
+ }
+
+ public class Constraints
+ {
+ public void Print1(T val) where T : class
+ {
+ val.ToString();
+ }
+
+ public void Print2(T val) where T : class?
+ {
+ if (val != null)
+ val.ToString();
+ }
+
+ public static void Print3() where T : Stream { }
+ public static void Print4() where T : IDisposable { }
+ }
+
+ public class Constraints2 where X: IComparable where Y : class?
+ {
+ public T Convert(T data) where T : IComparable => default;
+ public static void Print1() where T : Stream? { }
+ public static void Print2() where T : IDisposable? { }
+ }
+
+ public class NullableComparable : List, IComparable
+ {
+ public int CompareTo(string? other)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public class StringNullableList : List, IComparable where T : struct where U : class
+ {
+ public int CompareTo(U other) => 0;
+ }
+
+ public interface IDoStuff1
+ where TIn : notnull
+ where TOut : notnull
+ {
+ TOut DoStuff(TIn input);
+ }
+
+ public interface IDoStuff2
+ where TIn : class?
+ where TOut : notnull
+ {
+ TOut DoStuff(TIn input);
+ }
+
+ public interface IDoStuff3
+ {
+ TOut DoStuff(TIn input);
+ }
+
+ public interface IDoStuff4
+ where TIn : class?
+ where TOut : class
+ {
+ TOut DoStuff(TIn input);
+ }
+
+ public interface IDoStuff5
+ where TIn : class?
+ where TOut : struct
+ {
+ TOut DoStuff(TIn input);
+ }
+
+ public interface IDoStuff6
+ where TIn : notnull
+ where TOut : unmanaged
+ {
+ TOut DoStuff(TIn input);
+ }
+ }
+ // ReSharper restore ClassNeverInstantiated.Global
+ // ReSharper restore UnusedMember.Global
+}
diff --git a/src/PublicApiGeneratorTests/PublicApiGeneratorTests.csproj b/src/PublicApiGeneratorTests/PublicApiGeneratorTests.csproj
index f42561c..b088829 100644
--- a/src/PublicApiGeneratorTests/PublicApiGeneratorTests.csproj
+++ b/src/PublicApiGeneratorTests/PublicApiGeneratorTests.csproj
@@ -2,8 +2,13 @@
netcoreapp2.2
- $(NoWarn);CS0067
- latest
+ $(NoWarn);CS0067;IDE0051;IDE0060;IDE1006
+