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