public static void ParseComments(string scriptAssetFullPath, [NotNull] Dictionary <string, MemberCommentInfo> addToCollection) { #if DEV_MODE && PI_ASSERTATIONS UnityEngine.Debug.Assert(scriptAssetFullPath.IndexOf(':') != -1); #endif var commentXml = new StringBuilder(); var memberDefinition = new StringBuilder(); using (var reader = new StreamReader(scriptAssetFullPath)) { string line; while ((line = reader.ReadLine()) != null) { line = line.TrimStart(); if (line.StartsWith("///", StringComparison.Ordinal)) { commentXml.AppendLine(line.Substring(3)); } else if (commentXml.Length > 0) { memberDefinition.AppendLine(line); if (line.IndexOfAny(MemberDefinitionEndCharacters) != -1) { var info = new MemberCommentInfo(commentXml.ToString(), memberDefinition.ToString()); commentXml.Length = 0; memberDefinition.Length = 0; if (info.name.Length > 0 && info.comment.Length > 0) { if (addToCollection.ContainsKey(info.name)) { #if DEV_MODE UnityEngine.Debug.LogWarning("ParseComments(" + scriptAssetFullPath + ") : \"" + info.name + "\" key already in dictionary"); #endif continue; } addToCollection.Add(info.name, info); } else { info.Dispose(); } } } } } }
/// <summary> /// Attempts to find XML documentation file for dll that defines the given class type, parse XML documentation comments /// for all members of type, and populate the tooltips Dictionary with the results. /// </summary> /// <param name="classType"> Type of the class whose members' tooltips we want. This cannot be null. </param> /// <param name="tooltips"> [out] The member tooltips. If no XML documentation is found, this will be null. </param> /// <returns> True if XML documentation file was found and loaded successfully, false if not. </returns> public static bool TryGetMemberTooltips([NotNull] Type classType, [CanBeNull] out Dictionary <string, string> tooltips) { var assembly = classType.Assembly; XmlDocument xmlDocumentation; if (!CachedXMLDocuments.TryGetValue(assembly, out xmlDocumentation)) { xmlDocumentation = GetXMLDocument(assembly); CachedXMLDocuments[assembly] = xmlDocumentation; } if (xmlDocumentation == null) { tooltips = null; return(false); } var doc = xmlDocumentation["doc"]; if (doc == null) { #if DEV_MODE UnityEngine.Debug.LogWarning("XML Documentation for assembly " + classType.Assembly.GetName().Name + " had no \"doc\" section"); #endif tooltips = null; return(false); } var members = doc["members"]; if (members == null) { #if DEV_MODE UnityEngine.Debug.LogWarning("XML Documentation for assembly " + classType.Assembly.GetName().Name + " had no \"members\" section under \"doc\""); #endif tooltips = null; return(false); } tooltips = new Dictionary <string, string>(); // For example: // T:UnityEngine.BoxCollider (class type) // T:Sisus.OdinSerializer.QueueFormatter`2 (class with two generic types) // F:UnityEngine.Camera.onPreCull (field) // P:UnityEngine.BoxCollider.size (property) // M:UnityEngine.AI.NavMeshAgent.CompleteOffMeshLink (method with no parameters) // M:UnityEngine.Collider.ClosestPoint(UnityEngine.Vector3) (method with parameters) // M:Namespace.ClassName`1.MethodName (method inside generic class) // M:Namespace.ClassName`1.MethodName(`0) (method inside generic class with parameter of class generic type) // M:Namespace.ClassName.#ctor (constructor with no parameters) // M:Namespace.ClassName.MethodName``1(``0[]) (method with generic parameter) // M:Namespace.ClassName.MethodName``2(System.Collections.Generic.Dictionary{``0,``1}) (method with two generic parameters) string match = ":" + classType.FullName + "."; foreach (XmlElement member in members) { // skip members without attributes to avoid exceptions if (!member.HasAttributes) { continue; } var attributes = member.Attributes; var typePrefixAndFullName = attributes["name"].InnerText; if (typePrefixAndFullName.IndexOf(match, StringComparison.Ordinal) != -1) { MemberCommentInfo.ParseXmlComment(member, StringBuilder); var comment = StringBuilder.ToString(); StringBuilder.Length = 0; bool isMethod = typePrefixAndFullName[0] == 'M'; if (isMethod) { int methodParamsStart = typePrefixAndFullName.IndexOf('('); // handle special case of methods with parameters if (methodParamsStart != -1) { int methodNameStart = typePrefixAndFullName.LastIndexOf('.', methodParamsStart - 1) + 1; string methodNameAndParameters = typePrefixAndFullName.Substring(methodNameStart); #if CACHE_METHODS_USING_FULL_SIGNATURE tooltips[methodNameAndParameters] = comment; #else int parametersSectionLength = typePrefixAndFullName.Length - methodParamsStart; int methodNameLength = methodNameAndParameters.Length - parametersSectionLength; string methodNameOnly = methodNameAndParameters.Substring(0, methodNameLength); int genericTypeStart = methodNameOnly.IndexOf('`'); if (genericTypeStart != -1) { methodNameOnly = methodNameOnly.Substring(genericTypeStart); } tooltips[methodNameOnly] = comment; #endif continue; } #if !CACHE_METHODS_USING_FULL_SIGNATURE // handle special case of generic methods else { int methodNameStart = typePrefixAndFullName.LastIndexOf('.') + 1; string methodName = typePrefixAndFullName.Substring(methodNameStart); int genericTypeStart = methodName.IndexOf('`'); if (genericTypeStart != -1) { methodName = methodName.Substring(genericTypeStart); } tooltips[methodName] = comment; } #endif } int memberNameStart = typePrefixAndFullName.LastIndexOf('.') + 1; string memberName = typePrefixAndFullName.Substring(memberNameStart); tooltips[memberName] = comment; } } return(true); }
/// <summary> /// Tries to parse XML Documentation Comments from script file at path. /// </summary> /// <param name="scriptAssetPath"></param> /// <param name="addToCollection"></param> /// <param name="classTypeMustMatch"></param> /// <returns> False if failed, either because of write permissions, or because class definition was not found inside script asset. </returns> public static bool ParseComments(string scriptAssetPath, [NotNull] Dictionary <string, string> addToCollection, [CanBeNull] Type classTypeMustMatch) { #if DEV_MODE && PI_ASSERTATIONS UnityEngine.Debug.Assert(scriptAssetPath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase), scriptAssetPath); #endif if (classTypeMustMatch != null) { var mustBeFound = new List <string>(classTypeMustMatch.FullName.Split('.')); bool allFound = false; try { using (var reader = new StreamReader(scriptAssetPath)) { string line; int findCount = mustBeFound.Count; while ((line = reader.ReadLine()) != null) { for (int n = findCount - 1; n >= 0; n--) { if (line.IndexOf(mustBeFound[n], StringComparison.Ordinal) != -1) { mustBeFound.RemoveAt(n); findCount--; if (findCount == 0) { allFound = true; break; } } } if (allFound) { break; } } } } #if DEV_MODE catch (PathTooLongException e) { UnityEngine.Debug.LogError("ParseComments PathTooLongException. Path length was was " + scriptAssetPath.Length + ".\n" + e); return(false); } catch (DirectoryNotFoundException e) { UnityEngine.Debug.LogError("ParseComments DirectoryNotFoundException. Path length was " + scriptAssetPath.Length + ".\n" + e); return(false); } catch (Exception e) { UnityEngine.Debug.LogError("ParseComments " + e.GetType().Name + ". Path length was was " + scriptAssetPath.Length + ".\n" + e); return(false); } #else catch { return(false); } #endif if (!allFound) { #if DEV_MODE && DEBUG_FAIL_PARSE UnityEngine.Debug.LogWarning("Failed to find the following parts of full class name inside script asset: " + string.Join(", ", mustBeFound.ToArray()) + "\nasset path: " + scriptAssetFullPath); #endif return(false); } } var commentXml = StringBuilderPool.Create(); var memberDefinition = StringBuilderPool.Create(); try { using (var reader = new StreamReader(scriptAssetPath)) { string line; while ((line = reader.ReadLine()) != null) { line = line.TrimStart(); if (line.StartsWith("///", StringComparison.Ordinal)) { commentXml.AppendLine(line.Substring(3)); } else if (commentXml.Length > 0) { memberDefinition.AppendLine(line); if (line.IndexOfAny(MemberDefinitionEndCharacters) != -1) { var info = new MemberCommentInfo(commentXml.ToString(), memberDefinition.ToString()); commentXml.Length = 0; memberDefinition.Length = 0; if (info.name.Length > 0 && info.comment.Length > 0) { if (addToCollection.ContainsKey(info.name)) { #if DEV_MODE UnityEngine.Debug.LogWarning("ParseComments(" + scriptAssetPath + ") : \"" + info.name + "\" key already in dictionary"); #endif continue; } addToCollection.Add(info.name, info.comment); } else { info.Dispose(); } } } } } } #if DEV_MODE catch (PathTooLongException e) { UnityEngine.Debug.LogError("ParseComments PathTooLongException. Path \"" + scriptAssetPath + "\" length was " + scriptAssetPath.Length + ".\n" + e); StringBuilderPool.Dispose(ref commentXml); StringBuilderPool.Dispose(ref memberDefinition); return(false); } catch (DirectoryNotFoundException e) { UnityEngine.Debug.LogError("ParseComments DirectoryNotFoundException. Path was " + scriptAssetPath + ".\n" + e); StringBuilderPool.Dispose(ref commentXml); StringBuilderPool.Dispose(ref memberDefinition); return(false); } catch (Exception e) { UnityEngine.Debug.LogError("ParseComments " + e.GetType().Name + ". Path was " + scriptAssetPath + ".\n" + e); StringBuilderPool.Dispose(ref commentXml); StringBuilderPool.Dispose(ref memberDefinition); return(false); } #else catch { StringBuilderPool.Dispose(ref commentXml); StringBuilderPool.Dispose(ref memberDefinition); return(false); } #endif StringBuilderPool.Dispose(ref commentXml); StringBuilderPool.Dispose(ref memberDefinition); return(true); }