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 +