using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
namespace Semmle.Extraction.CSharp
{
///
/// Represents a .csproj file and reads information from it.
///
public class CsProjFile
{
private string Filename { get; }
private string Directory { get; }
///
/// Reads the .csproj file.
///
/// The .csproj file.
public CsProjFile(FileInfo filename)
{
Filename = filename.FullName;
var directoryName = Path.GetDirectoryName(Filename);
if (directoryName is null)
{
throw new Extraction.InternalError($"Directory of file '{Filename}' is null");
}
Directory = directoryName;
try
{
// This can fail if the .csproj is invalid or has
// unrecognised content or is the wrong version.
// This currently always fails on Linux because
// Microsoft.Build is not cross platform.
(csFiles, references, projectReferences) = ReadMsBuildProject(filename);
}
catch // lgtm[cs/catch-of-all-exceptions]
{
// There was some reason why the project couldn't be loaded.
// Fall back to reading the Xml document directly.
// This method however doesn't handle variable expansion.
(csFiles, references, projectReferences) = ReadProjectFileAsXml(filename, Directory);
}
}
///
/// Read the .csproj file using Microsoft Build.
/// This occasionally fails if the project file is incompatible for some reason,
/// and there seems to be no way to make it succeed. Fails on Linux.
///
/// The file to read.
private static (string[] csFiles, string[] references, string[] projectReferences) ReadMsBuildProject(FileInfo filename)
{
var msbuildProject = new Microsoft.Build.Execution.ProjectInstance(filename.FullName);
var references = msbuildProject.Items
.Where(item => item.ItemType == "Reference")
.Select(item => item.EvaluatedInclude)
.ToArray();
var projectReferences = msbuildProject.Items
.Where(item => item.ItemType == "ProjectReference")
.Select(item => item.EvaluatedInclude)
.ToArray();
var csFiles = msbuildProject.Items
.Where(item => item.ItemType == "Compile")
.Select(item => item.GetMetadataValue("FullPath"))
.Where(fn => fn.EndsWith(".cs"))
.ToArray();
return (csFiles, references, projectReferences);
}
///
/// Reads the .csproj file directly as XML.
/// This doesn't handle variables etc, and should only used as a
/// fallback if ReadMsBuildProject() fails.
///
/// The .csproj file.
private static (string[] csFiles, string[] references, string[] projectReferences) ReadProjectFileAsXml(FileInfo fileName, string directoryName)
{
var projFile = new XmlDocument();
var mgr = new XmlNamespaceManager(projFile.NameTable);
mgr.AddNamespace("msbuild", "http://schemas.microsoft.com/developer/msbuild/2003");
projFile.Load(fileName.FullName);
var projDir = fileName.Directory;
var root = projFile.DocumentElement;
if (root is null)
{
throw new NotSupportedException("Project file without root is not supported.");
}
// Figure out if it's dotnet core
var netCoreProjectFile = root.GetAttribute("Sdk") == "Microsoft.NET.Sdk";
if (netCoreProjectFile)
{
var explicitCsFiles = root
.SelectNodes("/Project/ItemGroup/Compile/@Include", mgr)
?.NodeList()
.Select(node => node.Value)
.Select(cs => GetFullPath(cs, projDir))
.Where(s => s is not null)
?? Enumerable.Empty();
var additionalCsFiles = System.IO.Directory.GetFiles(directoryName, "*.cs", SearchOption.AllDirectories);
var projectReferences = root
.SelectNodes("/Project/ItemGroup/ProjectReference/@Include", mgr)
?.NodeList()
.Select(node => node.Value)
.Select(csproj => GetFullPath(csproj, projDir))
.Where(s => s is not null)
?? Enumerable.Empty();
#nullable disable warnings
return (explicitCsFiles.Concat(additionalCsFiles).ToArray(), Array.Empty(), projectReferences.ToArray());
#nullable restore warnings
}
var references = root
.SelectNodes("/msbuild:Project/msbuild:ItemGroup/msbuild:Reference/@Include", mgr)
?.NodeList()
.Select(node => node.Value)
.Where(s => s is not null)
.ToArray()
?? Array.Empty();
var relativeCsIncludes = root
.SelectNodes("/msbuild:Project/msbuild:ItemGroup/msbuild:Compile/@Include", mgr)
?.NodeList()
.Select(node => node.Value)
.ToArray()
?? Array.Empty();
var csFiles = relativeCsIncludes
.Select(cs => GetFullPath(cs, projDir))
.Where(s => s is not null)
.ToArray();
#nullable disable warnings
return (csFiles, references, Array.Empty());
#nullable restore warnings
}
private static string? GetFullPath(string? file, DirectoryInfo? projDir)
{
if (file is null)
{
return null;
}
return Path.GetFullPath(Path.Combine(projDir?.FullName ?? string.Empty, Path.DirectorySeparatorChar == '/' ? file.Replace("\\", "/") : file));
}
private readonly string[] references;
private readonly string[] projectReferences;
private readonly string[] csFiles;
///
/// The list of references as a list of assembly IDs.
///
public IEnumerable References => references;
///
/// The list of project references in full path format.
///
public IEnumerable ProjectReferences => projectReferences;
///
/// The list of C# source files in full path format.
///
public IEnumerable Sources => csFiles;
}
internal static class XmlNodeHelper
{
///
/// Helper to convert an XmlNodeList into an IEnumerable.
/// This allows it to be used with Linq.
///
/// The list to convert.
/// A more useful data type.
public static IEnumerable NodeList(this XmlNodeList list)
{
return list.OfType();
}
}
}