using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using Semmle.Util; using Semmle.Extraction.CSharp.Standalone; namespace Semmle.BuildAnalyser { /// /// The output of a build analysis. /// interface IBuildAnalysis { /// /// Full filepaths of external references. /// IEnumerable ReferenceFiles { get; } /// /// Full filepaths of C# source files from project files. /// IEnumerable ProjectSourceFiles { get; } /// /// Full filepaths of C# source files in the filesystem. /// IEnumerable AllSourceFiles { get; } /// /// The assembly IDs which could not be resolved. /// IEnumerable UnresolvedReferences { get; } /// /// List of source files referenced by projects but /// which were not found in the filesystem. /// IEnumerable MissingSourceFiles { get; } } /// /// Main implementation of the build analysis. /// class BuildAnalysis : IBuildAnalysis { readonly AssemblyCache assemblyCache; readonly NugetPackages nuget; readonly IProgressMonitor progressMonitor; HashSet usedReferences = new HashSet(); readonly HashSet usedSources = new HashSet(); readonly HashSet missingSources = new HashSet(); readonly Dictionary unresolvedReferences = new Dictionary(); readonly DirectoryInfo sourceDir; int failedProjects, succeededProjects; readonly string[] allSources; int conflictedReferences = 0; /// /// Performs a C# build analysis. /// /// Analysis options from the command line. /// Display of analysis progress. public BuildAnalysis(Options options, IProgressMonitor progress) { progressMonitor = progress; sourceDir = new DirectoryInfo(options.SrcDir); progressMonitor.FindingFiles(options.SrcDir); allSources = sourceDir.GetFiles("*.cs", SearchOption.AllDirectories). Select(d => d.FullName). Where(d => !options.ExcludesFile(d)). ToArray(); var dllDirNames = options.DllDirs.Select(Path.GetFullPath); if (options.UseNuGet) { nuget = new NugetPackages(sourceDir.FullName); ReadNugetFiles(); dllDirNames = dllDirNames.Concat(Enumerators.Singleton(nuget.PackageDirectory)); } // Find DLLs in the .Net Framework if (options.ScanNetFrameworkDlls) { dllDirNames = dllDirNames.Concat(Runtime.Runtimes.Take(1)); } assemblyCache = new BuildAnalyser.AssemblyCache(dllDirNames, progress); // Analyse all .csproj files in the source tree. if (options.SolutionFile != null) { AnalyseSolution(options.SolutionFile); } else if (options.AnalyseCsProjFiles) { AnalyseProjectFiles(); } if (!options.AnalyseCsProjFiles) { usedReferences = new HashSet(assemblyCache.AllAssemblies.Select(a => a.Filename)); } ResolveConflicts(); if (options.UseMscorlib) { UseReference(typeof(object).Assembly.Location); } // Output the findings foreach (var r in usedReferences) { progressMonitor.ResolvedReference(r); } foreach (var r in unresolvedReferences) { progressMonitor.UnresolvedReference(r.Key, r.Value); } progressMonitor.Summary( AllSourceFiles.Count(), ProjectSourceFiles.Count(), MissingSourceFiles.Count(), ReferenceFiles.Count(), UnresolvedReferences.Count(), conflictedReferences, succeededProjects + failedProjects, failedProjects); } /// /// Resolves conflicts between all of the resolved references. /// If the same assembly name is duplicated with different versions, /// resolve to the higher version number. /// void ResolveConflicts() { var sortedReferences = usedReferences. Select(r => assemblyCache.GetAssemblyInfo(r)). OrderBy(r => r.Version). ToArray(); Dictionary finalAssemblyList = new Dictionary(); // Pick the highest version for each assembly name foreach (var r in sortedReferences) finalAssemblyList[r.Name] = r; // Update the used references list usedReferences = new HashSet(finalAssemblyList.Select(r => r.Value.Filename)); // Report the results foreach (var r in sortedReferences) { var resolvedInfo = finalAssemblyList[r.Name]; if (resolvedInfo.Version != r.Version) { progressMonitor.ResolvedConflict(r.Id, resolvedInfo.Id); ++conflictedReferences; } } } /// /// Find and restore NuGet packages. /// void ReadNugetFiles() { nuget.FindPackages(); nuget.InstallPackages(progressMonitor); } /// /// Store that a particular reference file is used. /// /// The filename of the reference. void UseReference(string reference) { usedReferences.Add(reference); } /// /// Store that a particular source file is used (by a project file). /// /// The source file. void UseSource(FileInfo sourceFile) { if (sourceFile.Exists) { usedSources.Add(sourceFile.FullName); } else { missingSources.Add(sourceFile.FullName); } } /// /// The list of resolved reference files. /// public IEnumerable ReferenceFiles => this.usedReferences; /// /// The list of source files used in projects. /// public IEnumerable ProjectSourceFiles => usedSources; /// /// All of the source files in the source directory. /// public IEnumerable AllSourceFiles => allSources; /// /// List of assembly IDs which couldn't be resolved. /// public IEnumerable UnresolvedReferences => this.unresolvedReferences.Select(r => r.Key); /// /// List of source files which were mentioned in project files but /// do not exist on the file system. /// public IEnumerable MissingSourceFiles => missingSources; /// /// Record that a particular reference couldn't be resolved. /// Note that this records at most one project file per missing reference. /// /// The assembly ID. /// The project file making the reference. void UnresolvedReference(string id, string projectFile) { unresolvedReferences[id] = projectFile; } /// /// Performs an analysis of all .csproj files. /// void AnalyseProjectFiles() { AnalyseProjectFiles(sourceDir.GetFiles("*.csproj", SearchOption.AllDirectories)); } /// /// Reads all the source files and references from the given list of projects. /// /// The list of projects to analyse. void AnalyseProjectFiles(FileInfo[] projectFiles) { progressMonitor.AnalysingProjectFiles(projectFiles.Count()); foreach (var proj in projectFiles) { try { var csProj = new CsProjFile(proj); foreach (var @ref in csProj.References) { AssemblyInfo resolved = assemblyCache.ResolveReference(@ref); if (!resolved.Valid) { UnresolvedReference(@ref, proj.FullName); } else { UseReference(resolved.Filename); } } foreach (var src in csProj.Sources) { // Make a note of which source files the projects use. // This information doesn't affect the build but is dumped // as diagnostic output. UseSource(new FileInfo(src)); } ++succeededProjects; } catch (Exception ex) { ++failedProjects; progressMonitor.FailedProjectFile(proj.FullName, ex.Message); } } } /// /// Delete packages directory. /// public void Cleanup() { if (nuget != null) nuget.Cleanup(progressMonitor); } /// /// Analyse all project files in a given solution only. /// /// The filename of the solution. public void AnalyseSolution(string solutionFile) { var sln = new SolutionFile(solutionFile); AnalyseProjectFiles(sln.Projects.Select(p => new FileInfo(p)).ToArray()); } } }