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