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());
}
}
}