diff --git a/mdoc/Mono.Documentation/MDocUpdater.cs b/mdoc/Mono.Documentation/MDocUpdater.cs index 9dcaebb1f..e27949198 100644 --- a/mdoc/Mono.Documentation/MDocUpdater.cs +++ b/mdoc/Mono.Documentation/MDocUpdater.cs @@ -3946,7 +3946,7 @@ public void MakeParameters (XmlElement root, MemberReference member, IList attributes, bool shouldDuplicateWithNew = false) { XmlElement e = WriteElement (root, "ReturnValue"); - var valueToUse = GetDocTypeFullName (type, false); + var context = AttributeParserContext.Create ((ICustomAttributeProvider)member); + var isNullable = context.IsNullable (); + var valueToUse = GetDocTypeFullName (type, context, false); if ((type.IsRequiredModifier && ((RequiredModifierType)type).ElementType.IsByReference) || type.IsByReference) e.SetAttribute("RefType", "Ref"); @@ -4235,6 +4246,8 @@ private void MakeReturnValue (FrameworkTypeEntry typeEntry, XmlElement root, Typ valueToUse = valueToUse.Remove(valueToUse.Length - 1); } + if (isNullable) + valueToUse += DocUtils.GetTypeNullableSymbol (type, isNullable); DocUtils.AddElementWithFx( typeEntry, @@ -4422,12 +4435,22 @@ internal static string GetMemberType (MemberReference mi) private static string GetDocTypeName (TypeReference type, bool useTypeProjection = true) { - return docTypeFormatter.GetName (type, useTypeProjection: useTypeProjection); + return GetDocTypeName (type, EmptyAttributeParserContext.Empty (), useTypeProjection); + } + + private static string GetDocTypeName (TypeReference type, IAttributeParserContext context, bool useTypeProjection = true) + { + return docTypeFormatter.GetName (type, context, useTypeProjection: useTypeProjection); } internal static string GetDocTypeFullName (TypeReference type, bool useTypeProjection = true, bool isTypeofOperator = false) { - return DocTypeFullMemberFormatter.Default.GetName (type, useTypeProjection: useTypeProjection, isTypeofOperator: isTypeofOperator); + return GetDocTypeFullName (type, EmptyAttributeParserContext.Empty (), useTypeProjection, isTypeofOperator); + } + + internal static string GetDocTypeFullName (TypeReference type, IAttributeParserContext context, bool useTypeProjection = true, bool isTypeofOperator = false) + { + return DocTypeFullMemberFormatter.Default.GetName (type, context, useTypeProjection: useTypeProjection, isTypeofOperator: isTypeofOperator); } internal static string GetXPathForMember (DocumentationMember member) diff --git a/mdoc/Mono.Documentation/Updater/DocUtils.cs b/mdoc/Mono.Documentation/Updater/DocUtils.cs index 8a29d0104..7a6c67d46 100644 --- a/mdoc/Mono.Documentation/Updater/DocUtils.cs +++ b/mdoc/Mono.Documentation/Updater/DocUtils.cs @@ -945,5 +945,30 @@ public static TypeDefinition FixUnnamedParameters(TypeDefinition type) return type; } + + public static string GetTypeNullableSymbol(TypeReference type, bool? isNullableType) + { + if (isNullableType.IsTrue() && !IsValueTypeOrDefineByReference(type) && !type.FullName.Equals("System.Void")) + { + return "?"; + } + + return string.Empty; + } + + private static bool IsValueTypeOrDefineByReference(TypeReference type) + { + if (type.IsValueType) + { + return true; + } + + if (type is ByReferenceType byRefType) + { + return byRefType.ElementType.IsValueType; + } + + return false; + } } } diff --git a/mdoc/Mono.Documentation/Updater/Formatters/CSharpFullMemberFormatter.cs b/mdoc/Mono.Documentation/Updater/Formatters/CSharpFullMemberFormatter.cs index 7d0aa7c8f..295d66bcb 100644 --- a/mdoc/Mono.Documentation/Updater/Formatters/CSharpFullMemberFormatter.cs +++ b/mdoc/Mono.Documentation/Updater/Formatters/CSharpFullMemberFormatter.cs @@ -115,13 +115,12 @@ protected override string GetTypeName (TypeReference type, IAttributeParserConte return AppendSpecialGenericNullableValueTypeName (new StringBuilder (), genType, context, appendGeneric, useTypeProjection).ToString (); } - return base.GetTypeName (type, context, appendGeneric, useTypeProjection); + return base.GetTypeName (type, context, appendGeneric, useTypeProjection, isTypeofOperator); } protected override bool IsSpecialGenericNullableValueType (GenericInstanceType genInst) { - return genInst != null && (genInst.Name.StartsWith("ValueTuple`") || - (genInst.Name.StartsWith("Nullable`") && genInst.HasGenericArguments)); + return genInst != null && genInst.HasGenericArguments && (genInst.Name.StartsWith("ValueTuple`") || genInst.Name.StartsWith("Nullable`")); } protected override StringBuilder AppendSpecialGenericNullableValueTypeName (StringBuilder buf, GenericInstanceType genInst, IAttributeParserContext context, bool appendGeneric = true, bool useTypeProjection = true) @@ -431,27 +430,7 @@ protected override StringBuilder AppendMethodName (StringBuilder buf, MethodDefi protected override string GetTypeNullableSymbol(TypeReference type, bool? isNullableType) { - if (isNullableType.IsTrue() && !IsValueTypeOrDefineByReference(type) && !type.FullName.Equals("System.Void")) - { - return "?"; - } - - return string.Empty; - } - - private bool IsValueTypeOrDefineByReference(TypeReference type) - { - if (type.IsValueType) - { - return true; - } - - if (type is ByReferenceType byRefType) - { - return byRefType.ElementType.IsValueType; - } - - return false; + return DocUtils.GetTypeNullableSymbol(type, isNullableType); } protected override StringBuilder AppendGenericMethodConstraints (StringBuilder buf, MethodDefinition method) diff --git a/mdoc/Mono.Documentation/Updater/Formatters/DocTypeFullMemberFormatter.cs b/mdoc/Mono.Documentation/Updater/Formatters/DocTypeFullMemberFormatter.cs index 5c370b5d9..55c1d2a7c 100644 --- a/mdoc/Mono.Documentation/Updater/Formatters/DocTypeFullMemberFormatter.cs +++ b/mdoc/Mono.Documentation/Updater/Formatters/DocTypeFullMemberFormatter.cs @@ -1,6 +1,11 @@ -namespace Mono.Documentation.Updater +using Mono.Cecil; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Mono.Documentation.Updater { - class DocTypeFullMemberFormatter : MemberFormatter + public class DocTypeFullMemberFormatter : MemberFormatter { private static MemberFormatter defaultFormatter; public static MemberFormatter Default @@ -20,5 +25,60 @@ protected override string NestedTypeSeparator { get { return "+"; } } + + protected override string GetTypeNullableSymbol(TypeReference type, bool? isNullableType) + { + return DocUtils.GetTypeNullableSymbol(type, isNullableType); + } + + protected override string GetTypeName(TypeReference type, IAttributeParserContext context, bool appendGeneric = true, bool useTypeProjection = true, bool isTypeofOperator = false) + { + GenericInstanceType genType = type as GenericInstanceType; + if (IsSpecialGenericNullableValueType(genType)) + { + return AppendSpecialGenericNullableValueTypeName(new StringBuilder(), genType, context, appendGeneric, useTypeProjection).ToString(); + } + + return base.GetTypeName(type, context, appendGeneric, useTypeProjection, isTypeofOperator); + } + + protected override bool IsSpecialGenericNullableValueType(GenericInstanceType genInst) + { + return genInst != null && genInst.HasGenericArguments && (genInst.Name.StartsWith("ValueTuple`") || genInst.Name.StartsWith("Nullable`")); + } + + protected override StringBuilder AppendSpecialGenericNullableValueTypeName(StringBuilder buf, GenericInstanceType genInst, IAttributeParserContext context, bool appendGeneric = true, bool useTypeProjection = true) + { + if (genInst.Name.StartsWith("Nullable`") && genInst.HasGenericArguments) + { + var underlyingTypeName = GetTypeName(genInst.GenericArguments.First(), context, appendGeneric, useTypeProjection); + buf.Append($"System.Nullable<{underlyingTypeName}>"); + + return buf; + } + + if (genInst.Name.StartsWith("ValueTuple`")) + { + buf.Append("System.ValueTuple<"); + var genArgList = new List(); + foreach (var item in genInst.GenericArguments) + { + var isNullableType = false; + if (!item.IsValueType) + { + isNullableType = context.IsNullable(); + } + + var underlyingTypeName = GetTypeName(item, context, appendGeneric, useTypeProjection) + GetTypeNullableSymbol(item, isNullableType); + genArgList.Add(underlyingTypeName); + } + buf.Append(string.Join(",", genArgList)); + buf.Append(">"); + + return buf; + } + + return buf; + } } } \ No newline at end of file diff --git a/mdoc/Mono.Documentation/Updater/Formatters/MemberFormatter.cs b/mdoc/Mono.Documentation/Updater/Formatters/MemberFormatter.cs index d5f634e10..4612b3dbc 100644 --- a/mdoc/Mono.Documentation/Updater/Formatters/MemberFormatter.cs +++ b/mdoc/Mono.Documentation/Updater/Formatters/MemberFormatter.cs @@ -51,7 +51,7 @@ public virtual string GetName (MemberReference member, IAttributeParserContext c (member == null ? "null" : member.GetType ().ToString ())); } - protected virtual string GetTypeName (TypeReference type, bool appendGeneric = true, bool useTypeProjection = true) + protected string GetTypeName (TypeReference type, bool appendGeneric = true, bool useTypeProjection = true) { return GetTypeName (type, EmptyAttributeParserContext.Empty(), appendGeneric, useTypeProjection: useTypeProjection); } @@ -210,13 +210,13 @@ protected StringBuilder _AppendTypeName (StringBuilder buf, TypeReference type, protected virtual bool IsSpecialGenericNullableValueType(GenericInstanceType genInst) { - // For special C# nullable value type only, the CSharpFullMemberFormatter subclass will override the method. + // For special C# nullable value type only, the subclass CSharpFullMemberFormatter, and DocTypeFullMemberFormatter will override the method. return false; } protected virtual StringBuilder AppendSpecialGenericNullableValueTypeName(StringBuilder buf, GenericInstanceType genInst, IAttributeParserContext context, bool appendGeneric = true, bool useTypeProjection = true) { - // For special C# nullable value type only, the CSharpFullMemberFormatter subclass will override the method. + // For special C# nullable value type only, the subclass CSharpFullMemberFormatter, and DocTypeFullMemberFormatter will override the method. return buf; } diff --git a/mdoc/Mono.Documentation/Updater/NullableReferenceTypeProvider.cs b/mdoc/Mono.Documentation/Updater/NullableReferenceTypeProvider.cs index 8832a2a0f..8216464f3 100644 --- a/mdoc/Mono.Documentation/Updater/NullableReferenceTypeProvider.cs +++ b/mdoc/Mono.Documentation/Updater/NullableReferenceTypeProvider.cs @@ -117,6 +117,16 @@ private ICollection GetTypeNullableAttributes() return GetTypeNullableAttributes(fieldDefinition); } + if (provider is MethodDefinition methodDefinition) + { + return GetTypeNullableAttributes(methodDefinition); + } + + if (provider is EventDefinition eventDefinition) + { + return GetTypeNullableAttributes(eventDefinition); + } + throw new ArgumentException("We don't support this custom attribute provider type now.", nameof(provider)); } @@ -144,6 +154,11 @@ private ICollection GetTypeNullableAttributes(FieldDef }; } + private ICollection GetTypeNullableAttributes(EventDefinition eventDefinition) + { + return GetTypeNullableAttributes(eventDefinition.AddMethod.Parameters[0]); + } + private ICollection GetTypeNullableAttributes(ICustomAttributeProvider customAttributeProvider, MethodDefinition methodDefinition) { var resultList = new List @@ -151,6 +166,14 @@ private ICollection GetTypeNullableAttributes(ICustomA customAttributeProvider }; + resultList.AddRange(GetTypeNullableAttributes(methodDefinition)); + + return resultList; + } + + private ICollection GetTypeNullableAttributes(MethodDefinition methodDefinition) + { + var resultList = new List(); if (methodDefinition != null) { resultList.Add(methodDefinition); diff --git a/mdoc/mdoc.Test/DocTypeNullableReferenceTypesTests.cs b/mdoc/mdoc.Test/DocTypeNullableReferenceTypesTests.cs new file mode 100644 index 000000000..98a0c126e --- /dev/null +++ b/mdoc/mdoc.Test/DocTypeNullableReferenceTypesTests.cs @@ -0,0 +1,80 @@ +using Mono.Cecil; +using Mono.Documentation.Updater; +using NUnit.Framework; + +namespace mdoc.Test +{ + public class DocTypeNullableReferenceTypesTests : BasicFormatterTests + { + private const string NullableReferenceTypesAssemblyPath = "../../../../external/Test/mdoc.Test.NullableReferenceTypes.dll"; + + protected override MemberFormatter formatter => DocTypeFullMemberFormatter.Default; + + [TestCase("System.Int32", "ValueType")] + [TestCase("System.Nullable", "NullableValueType")] + [TestCase("System.ValueTuple", "ValueTupleOfValueType")] + [TestCase("System.Nullable>", "NullableValueTupleOfValueType")] + [TestCase("System.Collections.Generic.Dictionary>", "DictionaryOfReferenceTypeKeyAndDictionaryOfReferenceTypeValue")] + [TestCase("System.Collections.Generic.Dictionary?>?", "NullableDictionaryOfNullableReferenceTypeKeyAndNullableDictionaryOfNullableReferenceTypeValue")] + public void DocTypeReturnType(string returnType, string methodName) + { + // This is a common test for the return type of method, extension method, property, field, and operator overloading. + // They have the same process logic that we just test once the type of them. + var type = GetType(NullableReferenceTypesAssemblyPath, "mdoc.Test.NullableReferenceTypes.CommonType"); + var method = GetMethod(type, i => i.Name == methodName); + + var typeName = GetDocTypeName(method.MethodReturnType, method.ReturnType); + + Assert.AreEqual(returnType, typeName); + } + + [TestCase("System.EventHandler", "EventHandler")] + [TestCase("System.EventHandler?", "NullableGenericEventHandler")] + [TestCase("System.EventHandler?", "NullableGenericEventHandlerOfNullableEventArgs")] + public void DocTypeEventType(string returnType, string methodName) + { + var type = GetType(NullableReferenceTypesAssemblyPath, "mdoc.Test.NullableReferenceTypes.Event"); + var @event = GetEvent(type, methodName) as EventDefinition; + + var typeName = GetDocTypeName(@event, @event.EventType); + + Assert.AreEqual(returnType, typeName); + } + + [TestCase("NullableAndNonNullableValueType", "System.Int32", "System.Nullable", "System.Int32")] + [TestCase("NullableAndNonNullableReferenceType", "System.String", "System.String?", "System.String")] + [TestCase("NullableAndNonNullableInterfaceOfValueType", "System.Collections.Generic.ICollection", "System.Collections.Generic.ICollection?", "System.Collections.Generic.ICollection")] + [TestCase("NullableAndNonNullableInterfaceOfReferenceType", "System.Collections.Generic.ICollection", "System.Collections.Generic.ICollection?", "System.Collections.Generic.ICollection")] + public void DocTypeParameterType(string methodName, params string[] methodParameterType) + { + // This is a common test for the parameter type of constructor, method, extension method, delegate and operator overloading. + // They have the same process logic that we just test once the type of them. + var type = GetType(NullableReferenceTypesAssemblyPath, "mdoc.Test.NullableReferenceTypes.MethodParameter"); + var method = GetMethod(type, i => i.Name == methodName); + + for (int i = 0; i < method.Parameters.Count; i++) + { + var methodParameter = method.Parameters[i]; + var expectedParameterType = methodParameterType[i]; + + var typeName = GetDocTypeName(methodParameter, methodParameter.ParameterType); + + Assert.AreEqual(expectedParameterType, typeName); + } + } + + private string GetDocTypeName(ICustomAttributeProvider provider, TypeReference type) + { + var context = AttributeParserContext.Create(provider); + var isNullable = context.IsNullable(); + var typeName = DocTypeFullMemberFormatter.Default.GetName(type, context, useTypeProjection: false); + + if (isNullable) + { + typeName += DocUtils.GetTypeNullableSymbol(type, isNullable); + } + + return typeName; + } + } +} diff --git a/mdoc/mdoc.Test/mdoc.Test.csproj b/mdoc/mdoc.Test/mdoc.Test.csproj index 6f33aeb64..c32fb8447 100644 --- a/mdoc/mdoc.Test/mdoc.Test.csproj +++ b/mdoc/mdoc.Test/mdoc.Test.csproj @@ -77,6 +77,7 @@ +