88using System . Collections . Concurrent ;
99using System . Text ;
1010using System . Security . Cryptography ;
11+ using System . Text . RegularExpressions ;
1112
1213namespace Semmle . BuildAnalyser
1314{
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-
4615 /// <summary>
4716 /// Main implementation of the build analysis.
4817 /// </summary>
49- internal sealed class BuildAnalysis : IBuildAnalysis , IDisposable
18+ internal sealed partial class BuildAnalysis : IDisposable
5019 {
5120 private readonly AssemblyCache assemblyCache ;
52- private readonly IProgressMonitor progressMonitor ;
21+ private readonly ProgressMonitor progressMonitor ;
5322 private readonly IDictionary < string , bool > usedReferences = new ConcurrentDictionary < string , bool > ( ) ;
5423 private readonly IDictionary < string , bool > sources = new ConcurrentDictionary < string , bool > ( ) ;
5524 private readonly IDictionary < string , string > unresolvedReferences = new ConcurrentDictionary < string , string > ( ) ;
56- private int failedProjects , succeededProjects ;
25+ private int failedProjects ;
26+ private int succeededProjects ;
5727 private readonly string [ ] allSources ;
5828 private int conflictedReferences = 0 ;
29+ private readonly Options options ;
30+ private readonly DirectoryInfo sourceDir ;
31+ private readonly DotNet dotnet ;
5932
6033 /// <summary>
6134 /// Performs a C# build analysis.
6235 /// </summary>
6336 /// <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 )
6639 {
6740 var startTime = DateTime . Now ;
6841
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 ) ;
7145
72- progressMonitor . FindingFiles ( options . SrcDir ) ;
46+ try
47+ {
48+ this . dotnet = new DotNet ( progressMonitor ) ;
49+ }
50+ catch
51+ {
52+ progressMonitor . MissingDotNet ( ) ;
53+ throw ;
54+ }
7355
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 ) ;
7857
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" ) ;
8163
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 ( ) ;
9465
9566 // Find DLLs in the .Net Framework
9667 if ( options . ScanNetFrameworkDlls )
@@ -100,28 +71,41 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
10071 dllDirNames . Add ( runtimeLocation ) ;
10172 }
10273
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 )
10575 {
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 ) ) ;
10980
110- if ( options . UseNuGet )
81+ if ( options . UseNuGet )
82+ {
83+ dllDirNames . Add ( packageDirectory . DirInfo . FullName ) ;
84+ try
11185 {
112- RestoreSolutions ( solutions ) ;
86+ var nuget = new NugetPackages ( sourceDir . FullName , packageDirectory , progressMonitor ) ;
87+ nuget . InstallPackages ( ) ;
88+ }
89+ catch ( FileNotFoundException )
90+ {
91+ progressMonitor . MissingNuGet ( ) ;
11392 }
114- dllDirNames . Add ( packageDirectory . DirInfo . FullName ) ;
115- assemblyCache = new BuildAnalyser . AssemblyCache ( dllDirNames , progress ) ;
116- AnalyseSolutions ( solutions ) ;
11793
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+ }
120101 }
121102
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 ) )
123107 {
124- UseReference ( typeof ( object ) . Assembly . Location ) ;
108+ UseReference ( filename ) ;
125109 }
126110
127111 ResolveConflicts ( ) ;
@@ -149,6 +133,13 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
149133 DateTime . Now - startTime ) ;
150134 }
151135
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+
152143 /// <summary>
153144 /// Computes a unique temp directory for the packages associated
154145 /// with this source tree. Use a SHA1 of the directory name.
@@ -158,9 +149,7 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
158149 private static string ComputeTempDirectory ( string srcDir )
159150 {
160151 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 ) ;
164153 var sb = new StringBuilder ( ) ;
165154 foreach ( var b in sha . Take ( 8 ) )
166155 sb . AppendFormat ( "{0:x2}" , b ) ;
@@ -195,12 +184,15 @@ private void ResolveConflicts()
195184
196185 // Pick the highest version for each assembly name
197186 foreach ( var r in sortedReferences )
187+ {
198188 finalAssemblyList [ r . Name ] = r ;
199-
189+ }
200190 // Update the used references list
201191 usedReferences . Clear ( ) ;
202192 foreach ( var r in finalAssemblyList . Select ( r => r . Value . Filename ) )
193+ {
203194 UseReference ( r ) ;
195+ }
204196
205197 // Report the results
206198 foreach ( var r in sortedReferences )
@@ -278,7 +270,9 @@ private void UnresolvedReference(string id, string projectFile)
278270 private void AnalyseProjectFiles ( IEnumerable < FileInfo > projectFiles )
279271 {
280272 foreach ( var proj in projectFiles )
273+ {
281274 AnalyseProject ( proj ) ;
275+ }
282276 }
283277
284278 private void AnalyseProject ( FileInfo project )
@@ -324,36 +318,90 @@ private void AnalyseProject(FileInfo project)
324318
325319 }
326320
327- private void Restore ( string projectOrSolution )
321+ private bool Restore ( string target )
328322 {
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 )
331329 {
332- exit = DotNet . RestoreToDirectory ( projectOrSolution , packageDirectory . DirInfo . FullName ) ;
330+ Restore ( target ) ;
333331 }
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 )
335341 {
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+ }
337376 }
338377
339- switch ( exit )
378+ foreach ( var package in notYetDownloadedPackages )
340379 {
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+ }
350392
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+ }
354402 }
355403
356- public void AnalyseSolutions ( IEnumerable < string > solutions )
404+ private void AnalyseSolutions ( IEnumerable < string > solutions )
357405 {
358406 Parallel . ForEach ( solutions , new ParallelOptions { MaxDegreeOfParallelism = 4 } , solutionFile =>
359407 {
@@ -374,5 +422,8 @@ public void Dispose()
374422 {
375423 packageDirectory ? . Dispose ( ) ;
376424 }
425+
426+ [ GeneratedRegex ( "<PackageReference .*Include=\" (.*?)\" .*/>" , RegexOptions . IgnoreCase | RegexOptions . Compiled | RegexOptions . Singleline ) ]
427+ private static partial Regex PackageReference ( ) ;
377428 }
378429}
0 commit comments