Skip to content

Commit a0bad3c

Browse files
committed
C#: Re-factor FileContent to make it unit-testable and make an initializer class.
1 parent a362ce0 commit a0bad3c

File tree

2 files changed

+68
-19
lines changed

2 files changed

+68
-19
lines changed

csharp/extractor/Semmle.Extraction.CSharp.Standalone/FileContent.cs

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@ namespace Semmle.BuildAnalyser
1717
internal partial class FileContent
1818
{
1919
private readonly ProgressMonitor progressMonitor;
20-
private readonly TemporaryDirectory packageDirectory;
20+
private readonly IUnsafeFileReader unsafeFileReader;
2121
private readonly Func<IEnumerable<string>> getFiles;
22+
private readonly Func<HashSet<string>> getAlreadyDownloadedPackages;
2223
private readonly HashSet<string> notYetDownloadedPackages = new HashSet<string>();
23-
24-
private bool IsInitialized { get; set; } = false;
24+
private Initializer Initialize { get; init; }
2525

2626
public HashSet<string> NotYetDownloadedPackages
2727
{
2828
get
2929
{
30-
Initialize();
30+
Initialize.Run();
3131
return notYetDownloadedPackages;
3232
}
3333
}
@@ -45,18 +45,30 @@ public bool UseAspNetDlls
4545
{
4646
get
4747
{
48-
Initialize();
48+
Initialize.Run();
4949
return useAspNetDlls;
5050
}
5151
}
5252

53-
public FileContent(TemporaryDirectory packageDirectory, ProgressMonitor progressMonitor, Func<IEnumerable<string>> getFiles)
53+
internal FileContent(Func<HashSet<string>> getAlreadyDownloadedPackages,
54+
ProgressMonitor progressMonitor,
55+
Func<IEnumerable<string>> getFiles,
56+
IUnsafeFileReader unsafeFileReader)
5457
{
58+
this.getAlreadyDownloadedPackages = getAlreadyDownloadedPackages;
5559
this.progressMonitor = progressMonitor;
56-
this.packageDirectory = packageDirectory;
5760
this.getFiles = getFiles;
61+
this.unsafeFileReader = unsafeFileReader;
62+
Initialize = new Initializer(DoInitialize);
5863
}
5964

65+
66+
public FileContent(TemporaryDirectory packageDirectory, ProgressMonitor progressMonitor, Func<IEnumerable<string>> getFiles) : this(() => Directory.GetDirectories(packageDirectory.DirInfo.FullName)
67+
.Select(d => Path.GetFileName(d)
68+
.ToLowerInvariant())
69+
.ToHashSet(), progressMonitor, getFiles, new UnsafeFileReader())
70+
{ }
71+
6072
private static string GetGroup(ReadOnlySpan<char> input, ValueMatch valueMatch, string groupPrefix)
6173
{
6274
var match = input.Slice(valueMatch.Index, valueMatch.Length);
@@ -87,21 +99,14 @@ private static bool IsGroupMatch(ReadOnlySpan<char> line, Regex regex, string gr
8799
return false;
88100
}
89101

90-
private void Initialize()
102+
private void DoInitialize()
91103
{
92-
if (IsInitialized)
93-
{
94-
return;
95-
}
96-
97-
var alreadyDownloadedPackages = Directory.GetDirectories(packageDirectory.DirInfo.FullName).Select(d => Path.GetFileName(d).ToLowerInvariant()).ToHashSet();
104+
var alreadyDownloadedPackages = getAlreadyDownloadedPackages();
98105
foreach (var file in getFiles())
99106
{
100107
try
101108
{
102-
using var sr = new StreamReader(file);
103-
ReadOnlySpan<char> line;
104-
while ((line = sr.ReadLine()) != null)
109+
foreach (ReadOnlySpan<char> line in unsafeFileReader.ReadLines(file))
105110
{
106111

107112
// Find the not yet downloaded packages.
@@ -122,15 +127,13 @@ private void Initialize()
122127
IsGroupMatch(line, ProjectSdk(), "Sdk", "Microsoft.NET.Sdk.Web") ||
123128
IsGroupMatch(line, FrameworkReference(), "Include", "Microsoft.AspNetCore.App");
124129
}
125-
126130
}
127131
}
128132
catch (Exception ex)
129133
{
130134
progressMonitor.FailedToReadFile(file, ex);
131135
}
132136
}
133-
IsInitialized = true;
134137
}
135138

136139
[GeneratedRegex("<PackageReference.*\\sInclude=\"(.*?)\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
@@ -142,4 +145,22 @@ private void Initialize()
142145
[GeneratedRegex("<(.*\\s)?Project.*\\sSdk=\"(.*?)\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
143146
private static partial Regex ProjectSdk();
144147
}
148+
}
149+
150+
internal interface IUnsafeFileReader
151+
{
152+
IEnumerable<string> ReadLines(string file);
153+
}
154+
155+
internal class UnsafeFileReader : IUnsafeFileReader
156+
{
157+
public IEnumerable<string> ReadLines(string file)
158+
{
159+
using var sr = new StreamReader(file);
160+
string? line;
161+
while ((line = sr.ReadLine()) != null)
162+
{
163+
yield return line;
164+
}
165+
}
145166
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
3+
namespace Semmle.Util
4+
{
5+
/// <summary>
6+
/// An instance of this class is used to ensure that the provided
7+
/// action is executed only once and on the first call to `Run`.
8+
/// It is thread-safe.
9+
/// </summary>
10+
public class Initializer
11+
{
12+
private readonly Lazy<bool> doInit;
13+
14+
public Initializer(Action action)
15+
{
16+
doInit = new Lazy<bool>(() =>
17+
{
18+
action();
19+
return true;
20+
});
21+
}
22+
23+
public void Run()
24+
{
25+
var _ = doInit.Value;
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)