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

HicServices / RDMP / 9345538130

03 Jun 2024 06:46AM UTC coverage: 56.914%. Remained the same
9345538130

push

github

web-flow
Efficiency enhancements (#1829)

* More efficient checking for duplicates in lists

* GetValueOrDefault instead of manual fallback

* Update CommentStore.cs

Eliminate double-searching of CommentStore

* Eliminate more double-searches

* Update ExtractionConfiguration.cs

Clearer variable type, remove redundant casts

* Update ExecuteCommandAddPipelineComponent.cs

Make callbacks static for simplicity

* Update ColumnInfoANOPlan.cs

Join declaration and initialisation

* More GetValueOrDefault

* Simplify searching logic - Where(x).First() -> First(x) to avoid repeating copying

* Update CatalogueChildProvider.cs

Simplify list generation logic

* Update DleRunner.cs

Remove redundant counting of list size

* Null-coalesce, usings

* Lambdas

* Update YesNoYesToAllDialog.cs

Avoid thread-thrashing if yes or no to all has been clicked

* Method groups, static lambdas

* Avoid multiple lookups when iterating arrays

* Simplify null checks

* String interpolation, another loop deref

* Update CohortIdentificationTaskExecution.cs

Fix duplicate Dispose calls

* Update RemoteAttacher.cs

Make fake conditional value a constant

* Fix multiple enumerations

A

* Type check and cast combination

* Update RemoteAttacherTests.cs

String interpolation, object initialisers

* Turn some private fields to local variables where applicable

* No-op string interpolations

* Remove some redundant syntax

10817 of 20482 branches covered (52.81%)

Branch coverage included in aggregate %.

152 of 231 new or added lines in 53 files covered. (65.8%)

3 existing lines in 2 files now uncovered.

30827 of 52688 relevant lines covered (58.51%)

7412.54 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);
904✔
26

27
    private string[] _ignoreHelpFor =
904✔
28
    {
904✔
29
        "CsvHelper.xml",
904✔
30
        "Google.Protobuf.xml",
904✔
31
        "MySql.Data.xml",
904✔
32
        "Newtonsoft.Json.xml",
904✔
33
        "NLog.xml",
904✔
34
        "NuDoq.xml",
904✔
35
        "ObjectListView.xml",
904✔
36
        "QuickGraph.xml",
904✔
37
        "Renci.SshNet.xml",
904✔
38
        "ScintillaNET.xml",
904✔
39
        "nunit.framework.xml"
904✔
40
    };
904✔
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
            {
NEW
54
                using var zip = new LibArchiveReader(location);
×
NEW
55
                foreach (var xml in zip.Entries().Where(static xml => xml.Name.EndsWith(".xml", true, CultureInfo.InvariantCulture)))
×
56
                {
NEW
57
                    using var content = xml.Stream;
×
NEW
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)
382,340✔
78
            return;
2✔
79

80
        if (obj.Name != "member" || obj.Attributes == null) return;
685,818✔
81
        var memberName = obj.Attributes["name"]?.Value;
78,858!
82
        var summary = GetSummaryAsText(obj["summary"]);
78,858✔
83

84
        if (memberName == null || string.IsNullOrWhiteSpace(summary))
78,858✔
85
            return;
16,546✔
86

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

92
    private static string GetSummaryAsText(XmlElement summaryTag)
93
    {
94
        if (summaryTag == null)
78,858✔
95
            return null;
16,358✔
96

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

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

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

124
    private static string TrimSummary(string value) => value == null ? null : Regex.Replace(value, @"\s+", " ").Trim();
105,066!
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,478✔
136

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

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

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

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

153
        if (memberName.StartsWith("T:"))
43,936✔
154
            return GetLastTokens(memberName, 1);
40,468✔
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,696✔
162

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

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

171
        return sb;
7,064✔
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,492!
178
    }
33,746✔
179

180
    public bool ContainsKey(string keyword) => _dictionary.ContainsKey(keyword);
422✔
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
510✔
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];
170✔
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"))
170!
209
            docs = this[type.Name[..^"`1".Length]];
×
210

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

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

217
        if (formatAsParagraphs)
162✔
218
            docs = FormatAsParagraphs(docs);
156✔
219

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

222
        return docs.Length <= maxLength ? docs : $"{docs[..maxLength]}...";
162!
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);
170✔
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);
230✔
276
        message = Regex.Replace(message, @"(\.?[A-z]{2,}\.)+([A-z]+)", m => m.Groups[2].Value);
306✔
277

278
        return message;
230✔
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

© 2025 Coveralls, Inc