Skip to content

Commit 0e9f8c4

Browse files
authored
Merge pull request github#13876 from michaelnebel/csharp/aspnetdlls
C#: Include ASP.NET assemblies in the standalone extraction.
2 parents f5d7765 + 6ecbb40 commit 0e9f8c4

File tree

6 files changed

+348
-93
lines changed

6 files changed

+348
-93
lines changed

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

Lines changed: 28 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@
88
using System.Collections.Concurrent;
99
using System.Text;
1010
using System.Security.Cryptography;
11-
using System.Text.RegularExpressions;
1211

1312
namespace Semmle.BuildAnalyser
1413
{
1514
/// <summary>
1615
/// Main implementation of the build analysis.
1716
/// </summary>
18-
internal sealed partial class BuildAnalysis : IDisposable
17+
internal sealed class BuildAnalysis : IDisposable
1918
{
2019
private readonly AssemblyCache assemblyCache;
2120
private readonly ProgressMonitor progressMonitor;
@@ -29,6 +28,9 @@ internal sealed partial class BuildAnalysis : IDisposable
2928
private readonly Options options;
3029
private readonly DirectoryInfo sourceDir;
3130
private readonly DotNet dotnet;
31+
private readonly FileContent fileContent;
32+
private readonly TemporaryDirectory packageDirectory;
33+
3234

3335
/// <summary>
3436
/// Performs a C# build analysis.
@@ -55,6 +57,9 @@ public BuildAnalysis(Options options, ProgressMonitor progressMonitor)
5557

5658
this.progressMonitor.FindingFiles(options.SrcDir);
5759

60+
packageDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName));
61+
62+
this.fileContent = new FileContent(packageDirectory, progressMonitor, () => GetFiles("*.*"));
5863
this.allSources = GetFiles("*.cs").ToArray();
5964
var allProjects = GetFiles("*.csproj");
6065
var solutions = options.SolutionFile is not null
@@ -63,21 +68,26 @@ public BuildAnalysis(Options options, ProgressMonitor progressMonitor)
6368

6469
var dllDirNames = options.DllDirs.Select(Path.GetFullPath).ToList();
6570

66-
// Find DLLs in the .Net Framework
71+
// Find DLLs in the .Net / Asp.Net Framework
6772
if (options.ScanNetFrameworkDlls)
6873
{
69-
var runtimeLocation = new Runtime(dotnet).GetRuntime(options.UseSelfContainedDotnet);
70-
progressMonitor.Log(Util.Logging.Severity.Info, $"Runtime location selected: {runtimeLocation}");
74+
var runtime = new Runtime(dotnet);
75+
var runtimeLocation = runtime.GetRuntime(options.UseSelfContainedDotnet);
76+
progressMonitor.LogInfo($".NET runtime location selected: {runtimeLocation}");
7177
dllDirNames.Add(runtimeLocation);
78+
79+
if (fileContent.UseAspNetDlls && runtime.GetAspRuntime() is string aspRuntime)
80+
{
81+
progressMonitor.LogInfo($"ASP.NET runtime location selected: {aspRuntime}");
82+
dllDirNames.Add(aspRuntime);
83+
}
7284
}
7385

7486
if (options.UseMscorlib)
7587
{
7688
UseReference(typeof(object).Assembly.Location);
7789
}
7890

79-
packageDirectory = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName));
80-
8191
if (options.UseNuGet)
8292
{
8393
dllDirNames.Add(packageDirectory.DirInfo.FullName);
@@ -187,6 +197,7 @@ private void ResolveConflicts()
187197
{
188198
finalAssemblyList[r.Name] = r;
189199
}
200+
190201
// Update the used references list
191202
usedReferences.Clear();
192203
foreach (var r in finalAssemblyList.Select(r => r.Value.Filename))
@@ -210,24 +221,18 @@ private void ResolveConflicts()
210221
/// Store that a particular reference file is used.
211222
/// </summary>
212223
/// <param name="reference">The filename of the reference.</param>
213-
private void UseReference(string reference)
214-
{
215-
usedReferences[reference] = true;
216-
}
224+
private void UseReference(string reference) => usedReferences[reference] = true;
217225

218226
/// <summary>
219227
/// Store that a particular source file is used (by a project file).
220228
/// </summary>
221229
/// <param name="sourceFile">The source file.</param>
222-
private void UseSource(FileInfo sourceFile)
223-
{
224-
sources[sourceFile.FullName] = sourceFile.Exists;
225-
}
230+
private void UseSource(FileInfo sourceFile) => sources[sourceFile.FullName] = sourceFile.Exists;
226231

227232
/// <summary>
228233
/// The list of resolved reference files.
229234
/// </summary>
230-
public IEnumerable<string> ReferenceFiles => this.usedReferences.Keys;
235+
public IEnumerable<string> ReferenceFiles => usedReferences.Keys;
231236

232237
/// <summary>
233238
/// The list of source files used in projects.
@@ -242,7 +247,7 @@ private void UseSource(FileInfo sourceFile)
242247
/// <summary>
243248
/// List of assembly IDs which couldn't be resolved.
244249
/// </summary>
245-
public IEnumerable<string> UnresolvedReferences => this.unresolvedReferences.Select(r => r.Key);
250+
public IEnumerable<string> UnresolvedReferences => unresolvedReferences.Select(r => r.Key);
246251

247252
/// <summary>
248253
/// List of source files which were mentioned in project files but
@@ -256,12 +261,7 @@ private void UseSource(FileInfo sourceFile)
256261
/// </summary>
257262
/// <param name="id">The assembly ID.</param>
258263
/// <param name="projectFile">The project file making the reference.</param>
259-
private void UnresolvedReference(string id, string projectFile)
260-
{
261-
unresolvedReferences[id] = projectFile;
262-
}
263-
264-
private readonly TemporaryDirectory packageDirectory;
264+
private void UnresolvedReference(string id, string projectFile) => unresolvedReferences[id] = projectFile;
265265

266266
/// <summary>
267267
/// Reads all the source files and references from the given list of projects.
@@ -318,10 +318,8 @@ private void AnalyseProject(FileInfo project)
318318

319319
}
320320

321-
private bool Restore(string target, string? pathToNugetConfig = null)
322-
{
323-
return dotnet.RestoreToDirectory(target, packageDirectory.DirInfo.FullName, pathToNugetConfig);
324-
}
321+
private bool Restore(string target, string? pathToNugetConfig = null) =>
322+
dotnet.RestoreToDirectory(target, packageDirectory.DirInfo.FullName, pathToNugetConfig);
325323

326324
private void Restore(IEnumerable<string> targets, string? pathToNugetConfig = null)
327325
{
@@ -331,11 +329,9 @@ private void Restore(IEnumerable<string> targets, string? pathToNugetConfig = nu
331329
}
332330
}
333331

332+
334333
private void DownloadMissingPackages(IEnumerable<string> restoreTargets)
335334
{
336-
var alreadyDownloadedPackages = Directory.GetDirectories(packageDirectory.DirInfo.FullName).Select(d => Path.GetFileName(d).ToLowerInvariant()).ToHashSet();
337-
var notYetDownloadedPackages = new HashSet<string>();
338-
339335
var nugetConfigs = GetFiles("nuget.config", recurseSubdirectories: true).ToArray();
340336
string? nugetConfig = null;
341337
if (nugetConfigs.Length > 1)
@@ -352,46 +348,7 @@ private void DownloadMissingPackages(IEnumerable<string> restoreTargets)
352348
nugetConfig = nugetConfigs.FirstOrDefault();
353349
}
354350

355-
var allFiles = GetFiles("*.*");
356-
foreach (var file in allFiles)
357-
{
358-
try
359-
{
360-
using var sr = new StreamReader(file);
361-
ReadOnlySpan<char> line;
362-
while ((line = sr.ReadLine()) != null)
363-
{
364-
foreach (var valueMatch in PackageReference().EnumerateMatches(line))
365-
{
366-
// We can't get the group from the ValueMatch, so doing it manually:
367-
var match = line.Slice(valueMatch.Index, valueMatch.Length);
368-
var includeIndex = match.IndexOf("Include", StringComparison.InvariantCultureIgnoreCase);
369-
if (includeIndex == -1)
370-
{
371-
continue;
372-
}
373-
374-
match = match.Slice(includeIndex + "Include".Length + 1);
375-
376-
var quoteIndex1 = match.IndexOf("\"");
377-
var quoteIndex2 = match.Slice(quoteIndex1 + 1).IndexOf("\"");
378-
379-
var packageName = match.Slice(quoteIndex1 + 1, quoteIndex2).ToString().ToLowerInvariant();
380-
if (!alreadyDownloadedPackages.Contains(packageName))
381-
{
382-
notYetDownloadedPackages.Add(packageName);
383-
}
384-
}
385-
}
386-
}
387-
catch (Exception ex)
388-
{
389-
progressMonitor.FailedToReadFile(file, ex);
390-
continue;
391-
}
392-
}
393-
394-
foreach (var package in notYetDownloadedPackages)
351+
foreach (var package in fileContent.NotYetDownloadedPackages)
395352
{
396353
progressMonitor.NugetInstall(package);
397354
using var tempDir = new TemporaryDirectory(ComputeTempDirectory(package));
@@ -434,12 +391,6 @@ private void AnalyseSolutions(IEnumerable<string> solutions)
434391
});
435392
}
436393

437-
public void Dispose()
438-
{
439-
packageDirectory?.Dispose();
440-
}
441-
442-
[GeneratedRegex("<PackageReference .*Include=\"(.*?)\".*/>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
443-
private static partial Regex PackageReference();
394+
public void Dispose() => packageDirectory?.Dispose();
444395
}
445396
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ internal interface IDotNet
1010
bool RestoreToDirectory(string project, string directory, string? pathToNugetConfig = null);
1111
bool New(string folder);
1212
bool AddPackage(string folder, string package);
13-
public IList<string> GetListedRuntimes();
13+
IList<string> GetListedRuntimes();
1414
}
1515

1616
/// <summary>
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
using Semmle.Util;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Text.RegularExpressions;
7+
8+
namespace Semmle.BuildAnalyser
9+
{
10+
11+
// <summary>
12+
// This class is used to read a set of files and decide different properties about the
13+
// content (by reading the content of the files only once).
14+
// The implementation is lazy, so the properties are only calculated when
15+
// the first property is accessed.
16+
// </summary>
17+
internal partial class FileContent
18+
{
19+
private readonly ProgressMonitor progressMonitor;
20+
private readonly IUnsafeFileReader unsafeFileReader;
21+
private readonly Func<IEnumerable<string>> getFiles;
22+
private readonly Func<HashSet<string>> getAlreadyDownloadedPackages;
23+
private readonly HashSet<string> notYetDownloadedPackages = new HashSet<string>();
24+
private readonly Initializer initialize;
25+
26+
public HashSet<string> NotYetDownloadedPackages
27+
{
28+
get
29+
{
30+
initialize.Run();
31+
return notYetDownloadedPackages;
32+
}
33+
}
34+
35+
private bool useAspNetDlls = false;
36+
37+
/// <summary>
38+
/// True if any file in the source directory indicates that ASP.NET is used.
39+
/// The following heuristic is used to decide, if ASP.NET is used:
40+
/// If any file in the source directory contains something like (this will most like be a .csproj file)
41+
/// <Project Sdk="Microsoft.NET.Sdk.Web">
42+
/// <FrameworkReference Include="Microsoft.AspNetCore.App"/>
43+
/// </summary>
44+
public bool UseAspNetDlls
45+
{
46+
get
47+
{
48+
initialize.Run();
49+
return useAspNetDlls;
50+
}
51+
}
52+
53+
internal FileContent(Func<HashSet<string>> getAlreadyDownloadedPackages,
54+
ProgressMonitor progressMonitor,
55+
Func<IEnumerable<string>> getFiles,
56+
IUnsafeFileReader unsafeFileReader)
57+
{
58+
this.getAlreadyDownloadedPackages = getAlreadyDownloadedPackages;
59+
this.progressMonitor = progressMonitor;
60+
this.getFiles = getFiles;
61+
this.unsafeFileReader = unsafeFileReader;
62+
this.initialize = new Initializer(DoInitialize);
63+
}
64+
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+
72+
private static string GetGroup(ReadOnlySpan<char> input, ValueMatch valueMatch, string groupPrefix)
73+
{
74+
var match = input.Slice(valueMatch.Index, valueMatch.Length);
75+
var includeIndex = match.IndexOf(groupPrefix, StringComparison.InvariantCultureIgnoreCase);
76+
if (includeIndex == -1)
77+
{
78+
return string.Empty;
79+
}
80+
81+
match = match.Slice(includeIndex + groupPrefix.Length + 1);
82+
83+
var quoteIndex1 = match.IndexOf("\"");
84+
var quoteIndex2 = match.Slice(quoteIndex1 + 1).IndexOf("\"");
85+
86+
return match.Slice(quoteIndex1 + 1, quoteIndex2).ToString().ToLowerInvariant();
87+
}
88+
89+
private static bool IsGroupMatch(ReadOnlySpan<char> line, Regex regex, string groupPrefix, string value)
90+
{
91+
foreach (var valueMatch in regex.EnumerateMatches(line))
92+
{
93+
// We can't get the group from the ValueMatch, so doing it manually:
94+
if (GetGroup(line, valueMatch, groupPrefix) == value.ToLowerInvariant())
95+
{
96+
return true;
97+
}
98+
}
99+
return false;
100+
}
101+
102+
private void DoInitialize()
103+
{
104+
var alreadyDownloadedPackages = getAlreadyDownloadedPackages();
105+
foreach (var file in getFiles())
106+
{
107+
try
108+
{
109+
foreach (ReadOnlySpan<char> line in unsafeFileReader.ReadLines(file))
110+
{
111+
112+
// Find the not yet downloaded packages.
113+
foreach (var valueMatch in PackageReference().EnumerateMatches(line))
114+
{
115+
// We can't get the group from the ValueMatch, so doing it manually:
116+
var packageName = GetGroup(line, valueMatch, "Include");
117+
if (!string.IsNullOrEmpty(packageName) && !alreadyDownloadedPackages.Contains(packageName))
118+
{
119+
notYetDownloadedPackages.Add(packageName);
120+
}
121+
}
122+
123+
// Determine if ASP.NET is used.
124+
if (!useAspNetDlls)
125+
{
126+
useAspNetDlls =
127+
IsGroupMatch(line, ProjectSdk(), "Sdk", "Microsoft.NET.Sdk.Web") ||
128+
IsGroupMatch(line, FrameworkReference(), "Include", "Microsoft.AspNetCore.App");
129+
}
130+
}
131+
}
132+
catch (Exception ex)
133+
{
134+
progressMonitor.FailedToReadFile(file, ex);
135+
}
136+
}
137+
}
138+
139+
[GeneratedRegex("<PackageReference.*\\sInclude=\"(.*?)\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
140+
private static partial Regex PackageReference();
141+
142+
[GeneratedRegex("<FrameworkReference.*\\sInclude=\"(.*?)\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
143+
private static partial Regex FrameworkReference();
144+
145+
[GeneratedRegex("<(.*\\s)?Project.*\\sSdk=\"(.*?)\".*/?>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]
146+
private static partial Regex ProjectSdk();
147+
}
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+
}
166+
}

0 commit comments

Comments
 (0)