• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

HicServices / RDMP / 9859858140

09 Jul 2024 03:24PM UTC coverage: 56.679% (-0.2%) from 56.916%
9859858140

push

github

JFriel
update

10912 of 20750 branches covered (52.59%)

Branch coverage included in aggregate %.

30965 of 53135 relevant lines covered (58.28%)

7908.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.1
/Rdmp.Core/ReusableLibraryCode/Comments/CommentStore.cs
1
// Copyright (c) The University of Dundee 2018-2019
2
// This file is part of the Research Data Management Platform (RDMP).
3
// RDMP is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4
// RDMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
5
// You should have received a copy of the GNU General Public License along with RDMP. If not, see <https://www.gnu.org/licenses/>.
6

7
using System;
8
using System.Collections;
9
using System.Collections.Generic;
10
using System.Globalization;
11
using System.IO;
12
using System.Linq;
13
using System.Text;
14
using System.Text.RegularExpressions;
15
using System.Xml;
16
using LibArchive.Net;
17

18
namespace Rdmp.Core.ReusableLibraryCode.Comments;
19

20
/// <summary>
21
/// Records documentation for classes and keywords (e.g. foreign key names).
22
/// </summary>
23
public class CommentStore : IEnumerable<KeyValuePair<string, string>>
24
{
25
    private readonly Dictionary<string, string> _dictionary = new(StringComparer.CurrentCultureIgnoreCase);
914✔
26

27
    private string[] _ignoreHelpFor =
914✔
28
    {
914✔
29
        "CsvHelper.xml",
914✔
30
        "Google.Protobuf.xml",
914✔
31
        "MySql.Data.xml",
914✔
32
        "Newtonsoft.Json.xml",
914✔
33
        "NLog.xml",
914✔
34
        "NuDoq.xml",
914✔
35
        "ObjectListView.xml",
914✔
36
        "QuickGraph.xml",
914✔
37
        "Renci.SshNet.xml",
914✔
38
        "ScintillaNET.xml",
914✔
39
        "nunit.framework.xml"
914✔
40
    };
914✔
41

42
    public virtual void ReadComments(params string[] locations)
43
    {
44
        foreach (var location in locations.Where(static location => location is not null))
70✔
45
        {
46
            if (Directory.Exists(location))
14!
47
                foreach (var xml in Directory.EnumerateFiles(location, "*.xml", SearchOption.AllDirectories))
144✔
48
                {
49
                    using var content = File.OpenRead(xml);
58✔
50
                    ReadComments(content);
58✔
51
                }
52
            else if (File.Exists(location))
×
53
            {
54
                using var zip = new LibArchiveReader(location);
×
55
                foreach (var xml in zip.Entries().Where(static xml => xml.Name.EndsWith(".xml", true, CultureInfo.InvariantCulture)))
×
56
                {
57
                    using var content = xml.Stream;
×
58
                    ReadComments(content);
×
59
                }
60
            }
61
        }
62
    }
14✔
63

64
    private void ReadComments(Stream filename)
65
    {
66
        var doc = new XmlDocument();
58✔
67
        doc.Load(filename);
58✔
68
        doc.IterateThroughAllNodes(AddXmlDoc);
58✔
69
    }
58✔
70

71
    /// <summary>
72
    /// Adds the given member xml doc to the <see cref="CommentStore"/>
73
    /// </summary>
74
    /// <param name="obj"></param>
75
    public void AddXmlDoc(XmlNode obj)
76
    {
77
        if (obj == null)
383,462✔
78
            return;
2✔
79

80
        if (obj.Name != "member" || obj.Attributes == null) return;
687,764✔
81
        var memberName = obj.Attributes["name"]?.Value;
79,156!
82
        var summary = GetSummaryAsText(obj["summary"]);
79,156✔
83

84
        if (memberName == null || string.IsNullOrWhiteSpace(summary))
79,156✔
85
            return;
16,618✔
86

87
        //it's a Property get Type.Property (not fully specified)
88
        if (memberName.StartsWith("P:") || memberName.StartsWith("T:"))
62,538✔
89
            Add(GetLastTokens(memberName), summary.Trim());
33,744✔
90
    }
62,538✔
91

92
    private static string GetSummaryAsText(XmlElement summaryTag)
93
    {
94
        if (summaryTag == null)
79,156✔
95
            return null;
16,430✔
96

97
        var sb = new StringBuilder();
62,726✔
98

99
        summaryTag.IterateThroughAllNodes(
62,726✔
100
            n =>
62,726✔
101
            {
62,726✔
102
                switch (n.Name)
156,364✔
103
                {
62,726✔
104
                    case "see" when n.Attributes != null:
35,252✔
105
                        sb.Append($"{GetLastTokens(n.Attributes["cref"]?.Value)} "); // a <see cref="omg"> tag
35,252!
106
                        break;
35,252✔
107
                    case "para":
62,726✔
108
                        TrimEndSpace(sb)
7,366✔
109
                            .Append(Environment.NewLine +
7,366✔
110
                                    Environment.NewLine); //open para tag (next tag is probably #text)
7,366✔
111
                        break;
7,366✔
112
                    default:
62,726✔
113
                        {
62,726✔
114
                            if (n.Value != null) //e.g. #text
113,746✔
115
                                sb.Append($"{TrimSummary(n.Value)} ");
105,350✔
116
                            break;
62,726✔
117
                        }
62,726✔
118
                }
62,726✔
119
            });
176,472✔
120

121
        return sb.ToString();
62,726✔
122
    }
123

124
    private static string TrimSummary(string value) => value == null ? null : Regex.Replace(value, @"\s+", " ").Trim();
105,350!
125

126
    /// <summary>
127
    /// Returns the last x parts from a string like M:Abc.Def.Geh.AAA(fff,mm).  In this case it would return AAA for 1, Geh.AAA for 2 etc.
128
    /// </summary>
129
    /// <param name="memberName"></param>
130
    /// <param name="partsToGet"></param>
131
    /// <returns></returns>
132
    private static string GetLastTokens(string memberName, int partsToGet)
133
    {
134
        //throw away any preceding "T:", "M:" etc
135
        memberName = memberName[(memberName.IndexOf(':') + 1)..];
67,734✔
136

137
        var idxBracket = memberName.LastIndexOf('(');
67,734✔
138
        if (idxBracket != -1)
67,734✔
139
            memberName = memberName[..idxBracket];
1,266✔
140

141
        var matches = memberName.Split('.');
67,734✔
142

143
        return matches.Length < partsToGet
67,734!
144
            ? memberName
67,734✔
145
            : string.Join(".", matches.Reverse().Take(partsToGet).Reverse());
67,734✔
146
    }
147

148
    private static string GetLastTokens(string memberName)
149
    {
150
        if (memberName.StartsWith("P:"))
68,996✔
151
            return GetLastTokens(memberName, 2);
25,000✔
152

153
        if (memberName.StartsWith("T:"))
43,996✔
154
            return GetLastTokens(memberName, 1);
40,528✔
155

156
        return memberName.StartsWith("M:") ? GetLastTokens(memberName, 2) : memberName;
3,468✔
157
    }
158

159
    public static StringBuilder TrimEndSpace(StringBuilder sb)
160
    {
161
        if (sb == null || sb.Length == 0) return sb;
7,682✔
162

163
        var i = sb.Length - 1;
7,050✔
164
        for (; i >= 0; i--)
21,150✔
165
            if (sb[i] != ' ')
14,100✔
166
                break;
167

168
        if (i < sb.Length - 1)
7,050✔
169
            sb.Length = i + 1;
7,050✔
170

171
        return sb;
7,050✔
172
    }
173

174
    public void Add(string name, string summary)
175
    {
176
        //these are not helpful!
177
        if (name is not ("C" or "R")) _dictionary.TryAdd(name, summary);
67,876!
178
    }
33,938✔
179

180
    public bool ContainsKey(string keyword) => _dictionary.ContainsKey(keyword);
426✔
181

182
    /// <summary>
183
    /// Returns documentation for the keyword or null if no documentation exists
184
    /// </summary>
185
    /// <param name="index"></param>
186
    /// <returns></returns>
187
    public string this[string index] =>
188
        _dictionary.GetValueOrDefault(index); // Indexer declaration
518✔
189

190
    public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => _dictionary.GetEnumerator();
8✔
191

192
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
8✔
193

194
    /// <summary>
195
    /// Returns documentation for the class specified up to maxLength characters (after which ... is appended).  Returns null if no documentation exists for the class
196
    /// </summary>
197
    /// <param name="maxLength"></param>
198
    /// <param name="type"></param>
199
    /// <param name="allowInterfaceInstead">If no docs are found for Type X then look for IX too</param>
200
    /// <param name="formatAsParagraphs"></param>
201
    /// <returns></returns>
202
    public string GetTypeDocumentationIfExists(int maxLength, Type type, bool allowInterfaceInstead = true,
203
        bool formatAsParagraphs = false)
204
    {
205
        var docs = this[type.Name];
172✔
206

207
        //if it's a generic try looking for an non generic or abstract base etc
208
        if (docs == null && type.Name.EndsWith("`1"))
172!
209
            docs = this[type.Name[..^"`1".Length]];
×
210

211
        if (docs == null && allowInterfaceInstead && !type.IsInterface)
172✔
212
            docs = this[$"I{type.Name}"];
52✔
213

214
        if (string.IsNullOrWhiteSpace(docs))
172✔
215
            return null;
8✔
216

217
        if (formatAsParagraphs)
164✔
218
            docs = FormatAsParagraphs(docs);
158✔
219

220
        maxLength = Math.Max(10, maxLength - 3);
164✔
221

222
        return docs.Length <= maxLength ? docs : $"{docs[..maxLength]}...";
164!
223
    }
224

225
    /// <inheritdoc cref="GetTypeDocumentationIfExists(int,Type,bool,bool)"/>
226
    public string GetTypeDocumentationIfExists(Type type, bool allowInterfaceInstead = true,
227
        bool formatAsParagraphs = false) =>
228
        GetTypeDocumentationIfExists(int.MaxValue, type, allowInterfaceInstead, formatAsParagraphs);
172✔
229

230
    /// <summary>
231
    /// Searches the CommentStore for variations of the <paramref name="word"/> and returns the documentation if found (or null)
232
    /// </summary>
233
    /// <param name="word"></param>
234
    /// <param name="fuzzyMatch"></param>
235
    /// <param name="formatAsParagraphs">true to pass result string through <see cref="FormatAsParagraphs"/></param>
236
    /// <returns></returns>
237
    public string GetDocumentationIfExists(string word, bool fuzzyMatch, bool formatAsParagraphs = false)
238
    {
239
        var match = GetDocumentationKeywordIfExists(word, fuzzyMatch);
76✔
240

241
        return match == null ? null : formatAsParagraphs ? FormatAsParagraphs(this[match]) : this[match];
76!
242
    }
243

244
    /// <summary>
245
    /// Searches the CommentStore for variations of the <paramref name="word"/> and returns the key that matches (which might be word verbatim).
246
    /// 
247
    /// <para>This does not return the actual documentation, use <see cref="GetDocumentationIfExists"/> for that</para>
248
    /// </summary>
249
    /// <param name="word"></param>
250
    /// <param name="fuzzyMatch"></param>
251
    /// <returns></returns>
252
    public string GetDocumentationKeywordIfExists(string word, bool fuzzyMatch)
253
    {
254
        if (ContainsKey(word)) return word;
150✔
255
        if (!fuzzyMatch) return null;
2!
256

257
        //try the singular if we didn't match the plural
258
        if (word.EndsWith("s"))
2!
259
        {
260
            word = word.TrimEnd('s');
×
261
            if (ContainsKey(word)) return word;
×
262
        }
263

264
        word = $"I{word}";
2✔
265
        return ContainsKey(word) ? word : null;
2!
266
    }
267

268
    /// <summary>
269
    /// Formats a string read from xmldoc into paragraphs and gets rid of namespace prefixes introduced by cref="" notation.
270
    /// </summary>
271
    /// <param name="message"></param>
272
    /// <returns></returns>
273
    public static string FormatAsParagraphs(string message)
274
    {
275
        message = Regex.Replace(message, $"{Environment.NewLine}\\s*", Environment.NewLine + Environment.NewLine);
232✔
276
        message = Regex.Replace(message, @"(\.?[A-z]{2,}\.)+([A-z]+)", m => m.Groups[2].Value);
308✔
277

278
        return message;
232✔
279
    }
280
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc