1- using System ;
1+ using Semmle . Util ;
2+ using System ;
23using System . Collections . Generic ;
34using System . IO ;
45using System . Linq ;
5- using System . Runtime . InteropServices ;
6- using Semmle . Util ;
76using Semmle . Extraction . CSharp . Standalone ;
7+ using System . Threading . Tasks ;
8+ using System . Collections . Concurrent ;
9+ using System . Text ;
10+ using System . Security . Cryptography ;
811
912namespace Semmle . BuildAnalyser
1013{
@@ -43,19 +46,18 @@ interface IBuildAnalysis
4346 /// <summary>
4447 /// Main implementation of the build analysis.
4548 /// </summary>
46- class BuildAnalysis : IBuildAnalysis
49+ class BuildAnalysis : IBuildAnalysis , IDisposable
4750 {
48- readonly AssemblyCache assemblyCache ;
49- readonly NugetPackages nuget ;
50- readonly IProgressMonitor progressMonitor ;
51- HashSet < string > usedReferences = new HashSet < string > ( ) ;
52- readonly HashSet < string > usedSources = new HashSet < string > ( ) ;
53- readonly HashSet < string > missingSources = new HashSet < string > ( ) ;
54- readonly Dictionary < string , string > unresolvedReferences = new Dictionary < string , string > ( ) ;
55- readonly DirectoryInfo sourceDir ;
56- int failedProjects , succeededProjects ;
57- readonly string [ ] allSources ;
58- int conflictedReferences = 0 ;
51+ private readonly AssemblyCache assemblyCache ;
52+ private readonly NugetPackages nuget ;
53+ private readonly IProgressMonitor progressMonitor ;
54+ private readonly IDictionary < string , bool > usedReferences = new ConcurrentDictionary < string , bool > ( ) ;
55+ private readonly IDictionary < string , bool > sources = new ConcurrentDictionary < string , bool > ( ) ;
56+ private readonly IDictionary < string , string > unresolvedReferences = new ConcurrentDictionary < string , string > ( ) ;
57+ private readonly DirectoryInfo sourceDir ;
58+ private int failedProjects , succeededProjects ;
59+ private readonly string [ ] allSources ;
60+ private int conflictedReferences = 0 ;
5961
6062 /// <summary>
6163 /// Performs a C# build analysis.
@@ -64,6 +66,8 @@ class BuildAnalysis : IBuildAnalysis
6466 /// <param name="progress">Display of analysis progress.</param>
6567 public BuildAnalysis ( Options options , IProgressMonitor progress )
6668 {
69+ var startTime = DateTime . Now ;
70+
6771 progressMonitor = progress ;
6872 sourceDir = new DirectoryInfo ( options . SrcDir ) ;
6973
@@ -74,36 +78,43 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
7478 Where ( d => ! options . ExcludesFile ( d ) ) .
7579 ToArray ( ) ;
7680
77- var dllDirNames = options . DllDirs . Select ( Path . GetFullPath ) ;
81+ var dllDirNames = options . DllDirs . Select ( Path . GetFullPath ) . ToList ( ) ;
82+ PackageDirectory = new TemporaryDirectory ( ComputeTempDirectory ( sourceDir . FullName ) ) ;
7883
7984 if ( options . UseNuGet )
8085 {
81- nuget = new NugetPackages ( sourceDir . FullName ) ;
82- ReadNugetFiles ( ) ;
83- dllDirNames = dllDirNames . Concat ( Enumerators . Singleton ( nuget . PackageDirectory ) ) ;
86+ try
87+ {
88+ nuget = new NugetPackages ( sourceDir . FullName , PackageDirectory ) ;
89+ ReadNugetFiles ( ) ;
90+ }
91+ catch ( FileNotFoundException )
92+ {
93+ progressMonitor . MissingNuGet ( ) ;
94+ }
8495 }
8596
8697 // Find DLLs in the .Net Framework
8798 if ( options . ScanNetFrameworkDlls )
8899 {
89- dllDirNames = dllDirNames . Concat ( Runtime . Runtimes . Take ( 1 ) ) ;
100+ dllDirNames . Add ( Runtime . Runtimes . First ( ) ) ;
90101 }
91102
92- assemblyCache = new BuildAnalyser . AssemblyCache ( dllDirNames , progress ) ;
93-
94- // Analyse all .csproj files in the source tree.
95- if ( options . SolutionFile != null )
96- {
97- AnalyseSolution ( options . SolutionFile ) ;
98- }
99- else if ( options . AnalyseCsProjFiles )
103+ // These files can sometimes prevent `dotnet restore` from working correctly.
104+ using ( new FileRenamer ( sourceDir . GetFiles ( "global.json" , SearchOption . AllDirectories ) ) )
105+ using ( new FileRenamer ( sourceDir . GetFiles ( "Directory.Build.props" , SearchOption . AllDirectories ) ) )
100106 {
101- AnalyseProjectFiles ( ) ;
102- }
107+ var solutions = options . SolutionFile != null ?
108+ new [ ] { options . SolutionFile } :
109+ sourceDir . GetFiles ( "*.sln" , SearchOption . AllDirectories ) . Select ( d => d . FullName ) ;
103110
104- if ( ! options . AnalyseCsProjFiles )
105- {
106- usedReferences = new HashSet < string > ( assemblyCache . AllAssemblies . Select ( a => a . Filename ) ) ;
111+ RestoreSolutions ( solutions ) ;
112+ dllDirNames . Add ( PackageDirectory . DirInfo . FullName ) ;
113+ assemblyCache = new BuildAnalyser . AssemblyCache ( dllDirNames , progress ) ;
114+ AnalyseSolutions ( solutions ) ;
115+
116+ foreach ( var filename in assemblyCache . AllAssemblies . Select ( a => a . Filename ) )
117+ UseReference ( filename ) ;
107118 }
108119
109120 ResolveConflicts ( ) ;
@@ -114,7 +125,7 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
114125 }
115126
116127 // Output the findings
117- foreach ( var r in usedReferences )
128+ foreach ( var r in usedReferences . Keys )
118129 {
119130 progressMonitor . ResolvedReference ( r ) ;
120131 }
@@ -132,7 +143,27 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
132143 UnresolvedReferences . Count ( ) ,
133144 conflictedReferences ,
134145 succeededProjects + failedProjects ,
135- failedProjects ) ;
146+ failedProjects ,
147+ DateTime . Now - startTime ) ;
148+ }
149+
150+ /// <summary>
151+ /// Computes a unique temp directory for the packages associated
152+ /// with this source tree. Use a SHA1 of the directory name.
153+ /// </summary>
154+ /// <param name="srcDir"></param>
155+ /// <returns>The full path of the temp directory.</returns>
156+ private static string ComputeTempDirectory ( string srcDir )
157+ {
158+ var bytes = Encoding . Unicode . GetBytes ( srcDir ) ;
159+
160+ using var sha1 = new SHA1CryptoServiceProvider ( ) ;
161+ var sha = sha1 . ComputeHash ( bytes ) ;
162+ var sb = new StringBuilder ( ) ;
163+ foreach ( var b in sha . Take ( 8 ) )
164+ sb . AppendFormat ( "{0:x2}" , b ) ;
165+
166+ return Path . Combine ( Path . GetTempPath ( ) , "GitHub" , "packages" , sb . ToString ( ) ) ;
136167 }
137168
138169 /// <summary>
@@ -143,7 +174,7 @@ public BuildAnalysis(Options options, IProgressMonitor progress)
143174 void ResolveConflicts ( )
144175 {
145176 var sortedReferences = usedReferences .
146- Select ( r => assemblyCache . GetAssemblyInfo ( r ) ) .
177+ Select ( r => assemblyCache . GetAssemblyInfo ( r . Key ) ) .
147178 OrderBy ( r => r . Version ) .
148179 ToArray ( ) ;
149180
@@ -154,7 +185,9 @@ void ResolveConflicts()
154185 finalAssemblyList [ r . Name ] = r ;
155186
156187 // Update the used references list
157- usedReferences = new HashSet < string > ( finalAssemblyList . Select ( r => r . Value . Filename ) ) ;
188+ usedReferences . Clear ( ) ;
189+ foreach ( var r in finalAssemblyList . Select ( r => r . Value . Filename ) )
190+ UseReference ( r ) ;
158191
159192 // Report the results
160193 foreach ( var r in sortedReferences )
@@ -183,7 +216,7 @@ void ReadNugetFiles()
183216 /// <param name="reference">The filename of the reference.</param>
184217 void UseReference ( string reference )
185218 {
186- usedReferences . Add ( reference ) ;
219+ usedReferences [ reference ] = true ;
187220 }
188221
189222 /// <summary>
@@ -192,25 +225,18 @@ void UseReference(string reference)
192225 /// <param name="sourceFile">The source file.</param>
193226 void UseSource ( FileInfo sourceFile )
194227 {
195- if ( sourceFile . Exists )
196- {
197- usedSources . Add ( sourceFile . FullName ) ;
198- }
199- else
200- {
201- missingSources . Add ( sourceFile . FullName ) ;
202- }
228+ sources [ sourceFile . FullName ] = sourceFile . Exists ;
203229 }
204230
205231 /// <summary>
206232 /// The list of resolved reference files.
207233 /// </summary>
208- public IEnumerable < string > ReferenceFiles => this . usedReferences ;
234+ public IEnumerable < string > ReferenceFiles => this . usedReferences . Keys ;
209235
210236 /// <summary>
211237 /// The list of source files used in projects.
212238 /// </summary>
213- public IEnumerable < string > ProjectSourceFiles => usedSources ;
239+ public IEnumerable < string > ProjectSourceFiles => sources . Where ( s => s . Value ) . Select ( s => s . Key ) ;
214240
215241 /// <summary>
216242 /// All of the source files in the source directory.
@@ -226,7 +252,7 @@ void UseSource(FileInfo sourceFile)
226252 /// List of source files which were mentioned in project files but
227253 /// do not exist on the file system.
228254 /// </summary>
229- public IEnumerable < string > MissingSourceFiles => missingSources ;
255+ public IEnumerable < string > MissingSourceFiles => sources . Where ( s => ! s . Value ) . Select ( s => s . Key ) ;
230256
231257 /// <summary>
232258 /// Record that a particular reference couldn't be resolved.
@@ -239,74 +265,101 @@ void UnresolvedReference(string id, string projectFile)
239265 unresolvedReferences [ id ] = projectFile ;
240266 }
241267
242- /// <summary>
243- /// Performs an analysis of all .csproj files.
244- /// </summary>
245- void AnalyseProjectFiles ( )
246- {
247- AnalyseProjectFiles ( sourceDir . GetFiles ( "*.csproj" , SearchOption . AllDirectories ) ) ;
248- }
268+ readonly TemporaryDirectory PackageDirectory ;
249269
250270 /// <summary>
251271 /// Reads all the source files and references from the given list of projects.
252272 /// </summary>
253273 /// <param name="projectFiles">The list of projects to analyse.</param>
254- void AnalyseProjectFiles ( FileInfo [ ] projectFiles )
274+ void AnalyseProjectFiles ( IEnumerable < FileInfo > projectFiles )
255275 {
256- progressMonitor . AnalysingProjectFiles ( projectFiles . Count ( ) ) ;
257-
258276 foreach ( var proj in projectFiles )
277+ AnalyseProject ( proj ) ;
278+ }
279+
280+ void AnalyseProject ( FileInfo project )
281+ {
282+ if ( ! project . Exists )
259283 {
260- try
261- {
262- var csProj = new CsProjFile ( proj ) ;
284+ progressMonitor . MissingProject ( project . FullName ) ;
285+ return ;
286+ }
287+
288+ try
289+ {
290+ var csProj = new CsProjFile ( project ) ;
263291
264- foreach ( var @ref in csProj . References )
292+ foreach ( var @ref in csProj . References )
293+ {
294+ AssemblyInfo resolved = assemblyCache . ResolveReference ( @ref ) ;
295+ if ( ! resolved . Valid )
265296 {
266- AssemblyInfo resolved = assemblyCache . ResolveReference ( @ref ) ;
267- if ( ! resolved . Valid )
268- {
269- UnresolvedReference ( @ref , proj . FullName ) ;
270- }
271- else
272- {
273- UseReference ( resolved . Filename ) ;
274- }
297+ UnresolvedReference ( @ref , project . FullName ) ;
275298 }
276-
277- foreach ( var src in csProj . Sources )
299+ else
278300 {
279- // Make a note of which source files the projects use.
280- // This information doesn't affect the build but is dumped
281- // as diagnostic output.
282- UseSource ( new FileInfo ( src ) ) ;
301+ UseReference ( resolved . Filename ) ;
283302 }
284- ++ succeededProjects ;
285303 }
286- catch ( Exception ex ) // lgtm[cs/catch-of-all-exceptions]
304+
305+ foreach ( var src in csProj . Sources )
287306 {
288- ++ failedProjects ;
289- progressMonitor . FailedProjectFile ( proj . FullName , ex . Message ) ;
307+ // Make a note of which source files the projects use.
308+ // This information doesn't affect the build but is dumped
309+ // as diagnostic output.
310+ UseSource ( new FileInfo ( src ) ) ;
290311 }
312+
313+ ++ succeededProjects ;
314+ }
315+ catch ( Exception ex ) // lgtm[cs/catch-of-all-exceptions]
316+ {
317+ ++ failedProjects ;
318+ progressMonitor . FailedProjectFile ( project . FullName , ex . Message ) ;
291319 }
320+
292321 }
293322
294- /// <summary>
295- /// Delete packages directory.
296- /// </summary>
297- public void Cleanup ( )
323+ void Restore ( string projectOrSolution )
324+ {
325+ int exit = DotNet . RestoreToDirectory ( projectOrSolution , PackageDirectory . DirInfo . FullName ) ;
326+ switch ( exit )
327+ {
328+ case 0 :
329+ case 1 :
330+ // No errors
331+ break ;
332+ default :
333+ progressMonitor . CommandFailed ( "dotnet" , $ "restore \" { projectOrSolution } \" ", exit ) ;
334+ break ;
335+ }
336+ }
337+
338+ public void RestoreSolutions ( IEnumerable < string > solutions )
298339 {
299- if ( nuget != null ) nuget . Cleanup ( progressMonitor ) ;
340+ Parallel . ForEach ( solutions , new ParallelOptions { MaxDegreeOfParallelism = 4 } , Restore ) ;
300341 }
301342
302- /// <summary>
303- /// Analyse all project files in a given solution only.
304- /// </summary>
305- /// <param name="solutionFile">The filename of the solution.</param>
306- public void AnalyseSolution ( string solutionFile )
343+ public void AnalyseSolutions ( IEnumerable < string > solutions )
344+ {
345+ Parallel . ForEach ( solutions , new ParallelOptions { MaxDegreeOfParallelism = 4 } , solutionFile =>
346+ {
347+ try
348+ {
349+ var sln = new SolutionFile ( solutionFile ) ;
350+ progressMonitor . AnalysingSolution ( solutionFile ) ;
351+ AnalyseProjectFiles ( sln . Projects . Select ( p => new FileInfo ( p ) ) . Where ( p => p . Exists ) ) ;
352+ }
353+ catch ( Microsoft . Build . Exceptions . InvalidProjectFileException ex )
354+ {
355+ progressMonitor . FailedProjectFile ( solutionFile , ex . BaseMessage ) ;
356+ }
357+ } ) ;
358+ }
359+
360+ public void Dispose ( )
307361 {
308- var sln = new SolutionFile ( solutionFile ) ;
309- AnalyseProjectFiles ( sln . Projects . Select ( p => new FileInfo ( p ) ) . ToArray ( ) ) ;
362+ PackageDirectory ? . Dispose ( ) ;
310363 }
311364 }
312365}
0 commit comments