using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
namespace Semmle.Autobuild.Shared
{
///
/// Representation of a .proj file, a .csproj file (C#), or a .vcxproj file (C++).
/// C# project files come in 2 flavours, .Net core and msbuild, but they
/// have the same file extension.
///
public class Project : ProjectOrSolution where TAutobuildOptions : AutobuildOptionsShared
{
///
/// Holds if this project is for .Net core.
///
public bool DotNetProject { get; private set; }
public bool ValidToolsVersion { get; private set; }
public Version ToolsVersion { get; private set; }
private readonly Lazy>> includedProjectsLazy;
public override IEnumerable IncludedProjects => includedProjectsLazy.Value;
private static bool HasSdkAttribute(XmlElement xml) =>
xml.HasAttribute("Sdk");
private static bool AnyElement(XmlNodeList l, Func f) =>
l.OfType().Any(f);
///
/// According to https://learn.microsoft.com/en-us/visualstudio/msbuild/how-to-use-project-sdk?view=vs-2022#reference-a-project-sdk
/// there are three ways to reference a project SDK:
/// 1. As an attribute on the <Project/>.
/// 2. As a top level element of <Project>.
/// 3. As an attribute on an <Import> element.
///
/// Returns true, if the Sdk attribute is used, otherwise false.
///
private static bool ReferencesSdk(XmlElement xml) =>
HasSdkAttribute(xml) || // Case 1
AnyElement(xml.ChildNodes, e => e.Name == "Sdk") || // Case 2
AnyElement(xml.GetElementsByTagName("Import"), HasSdkAttribute); // Case 3
public Project(Autobuilder builder, string path) : base(builder, path)
{
ToolsVersion = new Version();
includedProjectsLazy = new Lazy>>(() => new List>());
if (!builder.Actions.FileExists(FullPath))
return;
XmlDocument projFile;
try
{
projFile = builder.Actions.LoadXml(FullPath);
}
catch (Exception ex) when (ex is XmlException || ex is FileNotFoundException)
{
builder.Logger.LogInfo($"Unable to read project file {path}.");
return;
}
var root = projFile.DocumentElement;
if (root?.Name == "Project")
{
if (ReferencesSdk(root))
{
DotNetProject = true;
return;
}
var toolsVersion = root.GetAttribute("ToolsVersion");
if (!string.IsNullOrEmpty(toolsVersion))
{
try
{
ToolsVersion = new Version(toolsVersion);
ValidToolsVersion = true;
}
catch // lgtm[cs/catch-of-all-exceptions]
// Generic catch clause - Version constructor throws about 5 different exceptions.
{
builder.Logger.LogWarning($"Project {path} has invalid tools version {toolsVersion}");
}
}
includedProjectsLazy = new Lazy>>(() =>
{
var ret = new List>();
// The documentation on `.proj` files is very limited, but it appears that both
// `` and `` is valid
var mgr = new XmlNamespaceManager(projFile.NameTable);
mgr.AddNamespace("msbuild", "http://schemas.microsoft.com/developer/msbuild/2003");
var projectFileIncludes = root.SelectNodes("//msbuild:Project/msbuild:ItemGroup/msbuild:ProjectFile/@Include", mgr)
?.OfType() ?? Array.Empty();
var projectFilesIncludes = root.SelectNodes("//msbuild:Project/msbuild:ItemGroup/msbuild:ProjectFiles/@Include", mgr)
?.OfType() ?? Array.Empty();
foreach (var include in projectFileIncludes.Concat(projectFilesIncludes))
{
if (include?.Value is null)
{
continue;
}
var includePath = builder.Actions.PathCombine(include.Value.Split('\\', StringSplitOptions.RemoveEmptyEntries));
ret.Add(new Project(builder, builder.Actions.PathCombine(DirectoryName, includePath)));
}
return ret;
});
}
}
}
}