8
8
using System . Collections . Concurrent ;
9
9
using System . Text ;
10
10
using System . Security . Cryptography ;
11
+ using System . Text . RegularExpressions ;
11
12
12
13
namespace Semmle . BuildAnalyser
13
14
{
14
- /// <summary>
15
- /// The output of a build analysis.
16
- /// </summary>
17
- internal interface IBuildAnalysis
18
- {
19
- /// <summary>
20
- /// Full filepaths of external references.
21
- /// </summary>
22
- IEnumerable < string > ReferenceFiles { get ; }
23
-
24
- /// <summary>
25
- /// Full filepaths of C# source files from project files.
26
- /// </summary>
27
- IEnumerable < string > ProjectSourceFiles { get ; }
28
-
29
- /// <summary>
30
- /// Full filepaths of C# source files in the filesystem.
31
- /// </summary>
32
- IEnumerable < string > AllSourceFiles { get ; }
33
-
34
- /// <summary>
35
- /// The assembly IDs which could not be resolved.
36
- /// </summary>
37
- IEnumerable < string > UnresolvedReferences { get ; }
38
-
39
- /// <summary>
40
- /// List of source files referenced by projects but
41
- /// which were not found in the filesystem.
42
- /// </summary>
43
- IEnumerable < string > MissingSourceFiles { get ; }
44
- }
45
-
46
15
/// <summary>
47
16
/// Main implementation of the build analysis.
48
17
/// </summary>
49
- internal sealed class BuildAnalysis : IBuildAnalysis , IDisposable
18
+ internal sealed partial class BuildAnalysis : IDisposable
50
19
{
51
20
private readonly AssemblyCache assemblyCache ;
52
- private readonly IProgressMonitor progressMonitor ;
21
+ private readonly ProgressMonitor progressMonitor ;
53
22
private readonly IDictionary < string , bool > usedReferences = new ConcurrentDictionary < string , bool > ( ) ;
54
23
private readonly IDictionary < string , bool > sources = new ConcurrentDictionary < string , bool > ( ) ;
55
24
private readonly IDictionary < string , string > unresolvedReferences = new ConcurrentDictionary < string , string > ( ) ;
56
- private int failedProjects , succeededProjects ;
25
+ private int failedProjects ;
26
+ private int succeededProjects ;
57
27
private readonly string [ ] allSources ;
58
28
private int conflictedReferences = 0 ;
29
+ private readonly Options options ;
30
+ private readonly DirectoryInfo sourceDir ;
31
+ private readonly DotNet dotnet ;
59
32
60
33
/// <summary>
61
34
/// Performs a C# build analysis.
62
35
/// </summary>
63
36
/// <param name="options">Analysis options from the command line.</param>
64
- /// <param name="progress ">Display of analysis progress.</param>
65
- public BuildAnalysis ( Options options , IProgressMonitor progress )
37
+ /// <param name="progressMonitor ">Display of analysis progress.</param>
38
+ public BuildAnalysis ( Options options , ProgressMonitor progressMonitor )
66
39
{
67
40
var startTime = DateTime . Now ;
68
41
69
- progressMonitor = progress ;
70
- var sourceDir = new DirectoryInfo ( options . SrcDir ) ;
42
+ this . options = options ;
43
+ this . progressMonitor = progressMonitor ;
44
+ this . sourceDir = new DirectoryInfo ( options . SrcDir ) ;
71
45
72
- progressMonitor . FindingFiles ( options . SrcDir ) ;
46
+ try
47
+ {
48
+ this . dotnet = new DotNet ( progressMonitor ) ;
49
+ }
50
+ catch
51
+ {
52
+ progressMonitor . MissingDotNet ( ) ;
53
+ throw ;
54
+ }
73
55
74
- allSources = sourceDir . GetFiles ( "*.cs" , SearchOption . AllDirectories )
75
- . Select ( d => d . FullName )
76
- . Where ( d => ! options . ExcludesFile ( d ) )
77
- . ToArray ( ) ;
56
+ this . progressMonitor . FindingFiles ( options . SrcDir ) ;
78
57
79
- var dllDirNames = options . DllDirs . Select ( Path . GetFullPath ) . ToList ( ) ;
80
- packageDirectory = new TemporaryDirectory ( ComputeTempDirectory ( sourceDir . FullName ) ) ;
58
+ this . allSources = GetFiles ( "*.cs" ) . ToArray ( ) ;
59
+ var allProjects = GetFiles ( "*.csproj" ) ;
60
+ var solutions = options . SolutionFile is not null
61
+ ? new [ ] { options . SolutionFile }
62
+ : GetFiles ( "*.sln" ) ;
81
63
82
- if ( options . UseNuGet )
83
- {
84
- try
85
- {
86
- var nuget = new NugetPackages ( sourceDir . FullName , packageDirectory ) ;
87
- nuget . InstallPackages ( progressMonitor ) ;
88
- }
89
- catch ( FileNotFoundException )
90
- {
91
- progressMonitor . MissingNuGet ( ) ;
92
- }
93
- }
64
+ var dllDirNames = options . DllDirs . Select ( Path . GetFullPath ) . ToList ( ) ;
94
65
95
66
// Find DLLs in the .Net Framework
96
67
if ( options . ScanNetFrameworkDlls )
@@ -100,28 +71,41 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
100
71
dllDirNames . Add ( runtimeLocation ) ;
101
72
}
102
73
103
- // TODO: remove the below when the required SDK is installed
104
- using ( new FileRenamer ( sourceDir . GetFiles ( "global.json" , SearchOption . AllDirectories ) ) )
74
+ if ( options . UseMscorlib )
105
75
{
106
- var solutions = options . SolutionFile is not null ?
107
- new [ ] { options . SolutionFile } :
108
- sourceDir . GetFiles ( "*.sln" , SearchOption . AllDirectories ) . Select ( d => d . FullName ) ;
76
+ UseReference ( typeof ( object ) . Assembly . Location ) ;
77
+ }
78
+
79
+ packageDirectory = new TemporaryDirectory ( ComputeTempDirectory ( sourceDir . FullName ) ) ;
109
80
110
- if ( options . UseNuGet )
81
+ if ( options . UseNuGet )
82
+ {
83
+ dllDirNames . Add ( packageDirectory . DirInfo . FullName ) ;
84
+ try
111
85
{
112
- RestoreSolutions ( solutions ) ;
86
+ var nuget = new NugetPackages ( sourceDir . FullName , packageDirectory , progressMonitor ) ;
87
+ nuget . InstallPackages ( ) ;
88
+ }
89
+ catch ( FileNotFoundException )
90
+ {
91
+ progressMonitor . MissingNuGet ( ) ;
113
92
}
114
- dllDirNames . Add ( packageDirectory . DirInfo . FullName ) ;
115
- assemblyCache = new BuildAnalyser . AssemblyCache ( dllDirNames , progress ) ;
116
- AnalyseSolutions ( solutions ) ;
117
93
118
- foreach ( var filename in assemblyCache . AllAssemblies . Select ( a => a . Filename ) )
119
- UseReference ( filename ) ;
94
+ // TODO: remove the below when the required SDK is installed
95
+ using ( new FileRenamer ( sourceDir . GetFiles ( "global.json" , SearchOption . AllDirectories ) ) )
96
+ {
97
+ Restore ( solutions ) ;
98
+ Restore ( allProjects ) ;
99
+ DownloadMissingPackages ( allProjects ) ;
100
+ }
120
101
}
121
102
122
- if ( options . UseMscorlib )
103
+ assemblyCache = new AssemblyCache ( dllDirNames , progressMonitor ) ;
104
+ AnalyseSolutions ( solutions ) ;
105
+
106
+ foreach ( var filename in assemblyCache . AllAssemblies . Select ( a => a . Filename ) )
123
107
{
124
- UseReference ( typeof ( object ) . Assembly . Location ) ;
108
+ UseReference ( filename ) ;
125
109
}
126
110
127
111
ResolveConflicts ( ) ;
@@ -149,6 +133,13 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
149
133
DateTime . Now - startTime ) ;
150
134
}
151
135
136
+ private IEnumerable < string > GetFiles ( string pattern )
137
+ {
138
+ return sourceDir . GetFiles ( pattern , SearchOption . AllDirectories )
139
+ . Select ( d => d . FullName )
140
+ . Where ( d => ! options . ExcludesFile ( d ) ) ;
141
+ }
142
+
152
143
/// <summary>
153
144
/// Computes a unique temp directory for the packages associated
154
145
/// with this source tree. Use a SHA1 of the directory name.
@@ -158,9 +149,7 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
158
149
private static string ComputeTempDirectory ( string srcDir )
159
150
{
160
151
var bytes = Encoding . Unicode . GetBytes ( srcDir ) ;
161
-
162
- using var sha1 = SHA1 . Create ( ) ;
163
- var sha = sha1 . ComputeHash ( bytes ) ;
152
+ var sha = SHA1 . HashData ( bytes ) ;
164
153
var sb = new StringBuilder ( ) ;
165
154
foreach ( var b in sha . Take ( 8 ) )
166
155
sb . AppendFormat ( "{0:x2}" , b ) ;
@@ -195,12 +184,15 @@ private void ResolveConflicts()
195
184
196
185
// Pick the highest version for each assembly name
197
186
foreach ( var r in sortedReferences )
187
+ {
198
188
finalAssemblyList [ r . Name ] = r ;
199
-
189
+ }
200
190
// Update the used references list
201
191
usedReferences . Clear ( ) ;
202
192
foreach ( var r in finalAssemblyList . Select ( r => r . Value . Filename ) )
193
+ {
203
194
UseReference ( r ) ;
195
+ }
204
196
205
197
// Report the results
206
198
foreach ( var r in sortedReferences )
@@ -278,7 +270,9 @@ private void UnresolvedReference(string id, string projectFile)
278
270
private void AnalyseProjectFiles ( IEnumerable < FileInfo > projectFiles )
279
271
{
280
272
foreach ( var proj in projectFiles )
273
+ {
281
274
AnalyseProject ( proj ) ;
275
+ }
282
276
}
283
277
284
278
private void AnalyseProject ( FileInfo project )
@@ -324,36 +318,90 @@ private void AnalyseProject(FileInfo project)
324
318
325
319
}
326
320
327
- private void Restore ( string projectOrSolution )
321
+ private bool Restore ( string target )
328
322
{
329
- int exit ;
330
- try
323
+ return dotnet . RestoreToDirectory ( target , packageDirectory . DirInfo . FullName ) ;
324
+ }
325
+
326
+ private void Restore ( IEnumerable < string > targets )
327
+ {
328
+ foreach ( var target in targets )
331
329
{
332
- exit = DotNet . RestoreToDirectory ( projectOrSolution , packageDirectory . DirInfo . FullName ) ;
330
+ Restore ( target ) ;
333
331
}
334
- catch ( FileNotFoundException )
332
+ }
333
+
334
+ private void DownloadMissingPackages ( IEnumerable < string > restoreTargets )
335
+ {
336
+ var alreadyDownloadedPackages = Directory . GetDirectories ( packageDirectory . DirInfo . FullName ) . Select ( d => Path . GetFileName ( d ) . ToLowerInvariant ( ) ) . ToHashSet ( ) ;
337
+ var notYetDownloadedPackages = new HashSet < string > ( ) ;
338
+
339
+ var allFiles = GetFiles ( "*.*" ) . ToArray ( ) ;
340
+ foreach ( var file in allFiles )
335
341
{
336
- exit = 2 ;
342
+ try
343
+ {
344
+ using var sr = new StreamReader ( file ) ;
345
+ ReadOnlySpan < char > line ;
346
+ while ( ( line = sr . ReadLine ( ) ) != null )
347
+ {
348
+ foreach ( var valueMatch in PackageReference ( ) . EnumerateMatches ( line ) )
349
+ {
350
+ // We can't get the group from the ValueMatch, so doing it manually:
351
+ var match = line . Slice ( valueMatch . Index , valueMatch . Length ) ;
352
+ var includeIndex = match . IndexOf ( "Include" , StringComparison . InvariantCultureIgnoreCase ) ;
353
+ if ( includeIndex == - 1 )
354
+ {
355
+ continue ;
356
+ }
357
+
358
+ match = match . Slice ( includeIndex + "Include" . Length + 1 ) ;
359
+
360
+ var quoteIndex1 = match . IndexOf ( "\" " ) ;
361
+ var quoteIndex2 = match . Slice ( quoteIndex1 + 1 ) . IndexOf ( "\" " ) ;
362
+
363
+ var packageName = match . Slice ( quoteIndex1 + 1 , quoteIndex2 ) . ToString ( ) . ToLowerInvariant ( ) ;
364
+ if ( ! alreadyDownloadedPackages . Contains ( packageName ) )
365
+ {
366
+ notYetDownloadedPackages . Add ( packageName ) ;
367
+ }
368
+ }
369
+ }
370
+ }
371
+ catch ( Exception ex )
372
+ {
373
+ progressMonitor . FailedToReadFile ( file , ex ) ;
374
+ continue ;
375
+ }
337
376
}
338
377
339
- switch ( exit )
378
+ foreach ( var package in notYetDownloadedPackages )
340
379
{
341
- case 0 :
342
- case 1 :
343
- // No errors
344
- break ;
345
- default :
346
- progressMonitor . CommandFailed ( "dotnet" , $ "restore \" { projectOrSolution } \" ", exit ) ;
347
- break ;
348
- }
349
- }
380
+ progressMonitor . NugetInstall ( package ) ;
381
+ using var tempDir = new TemporaryDirectory ( ComputeTempDirectory ( package ) ) ;
382
+ var success = dotnet . New ( tempDir . DirInfo . FullName ) ;
383
+ if ( ! success )
384
+ {
385
+ continue ;
386
+ }
387
+ success = dotnet . AddPackage ( tempDir . DirInfo . FullName , package ) ;
388
+ if ( ! success )
389
+ {
390
+ continue ;
391
+ }
350
392
351
- public void RestoreSolutions ( IEnumerable < string > solutions )
352
- {
353
- Parallel . ForEach ( solutions , new ParallelOptions { MaxDegreeOfParallelism = 4 } , Restore ) ;
393
+ success = Restore ( tempDir . DirInfo . FullName ) ;
394
+
395
+ // TODO: the restore might fail, we could retry with a prerelease (*-* instead of *) version of the package.
396
+
397
+ if ( ! success )
398
+ {
399
+ progressMonitor . FailedToRestoreNugetPackage ( package ) ;
400
+ }
401
+ }
354
402
}
355
403
356
- public void AnalyseSolutions ( IEnumerable < string > solutions )
404
+ private void AnalyseSolutions ( IEnumerable < string > solutions )
357
405
{
358
406
Parallel . ForEach ( solutions , new ParallelOptions { MaxDegreeOfParallelism = 4 } , solutionFile =>
359
407
{
@@ -374,5 +422,8 @@ public void Dispose()
374
422
{
375
423
packageDirectory ? . Dispose ( ) ;
376
424
}
425
+
426
+ [ GeneratedRegex ( "<PackageReference .*Include=\" (.*?)\" .*/>" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
427
+ private static partial Regex PackageReference ( ) ;
377
428
}
378
429
}
0 commit comments