File: Evaluation\Preprocessor.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Microsoft.Build.Construction;
using Microsoft.Build.Shared;
 
#nullable disable
 
namespace Microsoft.Build.Evaluation
{
    /// <summary>
    /// Creates a view of an evaluated project's XML as if it had all been loaded from
    /// a single file, instead of being assembled by pulling in imported files as it actually was.
    /// </summary>
    /// <remarks>
    /// Ideally the result would be buildable on its own, and *usually* this should be the case.
    /// Known cases where it wouldn't be buildable:
    /// -- $(MSBuildThisFile) and similar properties aren't corrected
    /// -- relative path in exists(..) conditions is relative to the imported file
    /// -- same for AssemblyFile on UsingTask
    /// Paths in item includes are relative to the importing project, though.
    /// </remarks>
    internal class Preprocessor
    {
        /// <summary>
        /// Project to preprocess
        /// </summary>
        private readonly Project _project;
 
        /// <summary>
        /// Table to resolve import tags
        /// </summary>
        private readonly Dictionary<XmlElement, IList<ProjectRootElement>> _importTable;
 
        /// <summary>
        /// Stack of file paths pushed as we follow imports
        /// </summary>
        private readonly Stack<string> _filePaths = new Stack<string>();
 
        /// <summary>
        /// Used to keep track of nodes that were added to the document from implicit imports which will be removed later.
        /// At the time of adding this feature, cloning is buggy so it is easier to just edit the DOM in memory.
        /// </summary>
        private List<XmlNode> _addedNodes;
 
        /// <summary>
        /// Table of implicit imports by document.  The list per document contains both top and bottom imports.
        /// </summary>
        private readonly Dictionary<XmlDocument, List<ResolvedImport>> _implicitImportsByProject = new Dictionary<XmlDocument, List<ResolvedImport>>();
 
        /// <summary>
        /// Constructor
        /// </summary>
        private Preprocessor(Project project)
        {
            _project = project;
 
            IList<ResolvedImport> imports = project.Imports;
 
            _importTable = new Dictionary<XmlElement, IList<ProjectRootElement>>(imports.Count);
 
            foreach (ResolvedImport entry in imports)
            {
                AddToImportTable(entry.ImportingElement.XmlElement, entry.ImportedProject);
            }
        }
 
        /// <summary>
        /// Returns an XmlDocument representing the evaluated project's XML as if it all had
        /// been loaded from a single file, instead of being assembled by pulling in imported files.
        /// </summary>
        internal static XmlDocument GetPreprocessedDocument(Project project)
        {
            Preprocessor preprocessor = new Preprocessor(project);
 
            XmlDocument result = preprocessor.Preprocess();
 
            return result;
        }
 
        /// <summary>
        /// Root of the preprocessing.
        /// </summary>
        private XmlDocument Preprocess()
        {
            XmlDocument outerDocument = _project.Xml.XmlDocument;
 
            CreateImplicitImportTable();
 
            AddImplicitImportNodes(outerDocument.DocumentElement);
 
            XmlDocument destinationDocument = (XmlDocument)outerDocument.CloneNode(false /* shallow */);
 
            _filePaths.Push(_project.FullPath);
 
            if (!String.IsNullOrEmpty(_project.FullPath)) // Ignore in-memory projects
            {
                destinationDocument.AppendChild(destinationDocument.CreateComment("\r\n" + new String('=', 140) + "\r\n" + _project.FullPath.Replace("--", "__") + "\r\n" + new String('=', 140) + "\r\n"));
            }
 
            CloneChildrenResolvingImports(outerDocument, destinationDocument);
 
            // Remove the nodes that were added as implicit imports
            //
            foreach (XmlNode node in _addedNodes)
            {
                node.ParentNode?.RemoveChild(node);
            }
 
            return destinationDocument;
        }
 
        private void AddToImportTable(XmlElement element, ProjectRootElement importedProject)
        {
            IList<ProjectRootElement> list;
            if (!_importTable.TryGetValue(element, out list))
            {
                list = new List<ProjectRootElement>();
                _importTable[element] = list;
            }
 
            list.Add(importedProject);
        }
 
        /// <summary>
        /// Creates a table containing implicit imports by project document.
        /// </summary>
        private void CreateImplicitImportTable()
        {
            int implicitImportCount = 0;
 
            // Loop through all implicit imports top and bottom
            foreach (ResolvedImport resolvedImport in _project.Imports.Where(i => i.ImportingElement.ImplicitImportLocation != ImplicitImportLocation.None))
            {
                implicitImportCount++;
                List<ResolvedImport> imports;
 
                // Attempt to get an existing list from the dictionary
                if (!_implicitImportsByProject.TryGetValue(resolvedImport.ImportingElement.XmlDocument, out imports))
                {
                    // Add a new list
                    _implicitImportsByProject[resolvedImport.ImportingElement.XmlDocument] = new List<ResolvedImport>();
 
                    // Get a pointer to the list
                    imports = _implicitImportsByProject[resolvedImport.ImportingElement.XmlDocument];
                }
 
                imports.Add(resolvedImport);
            }
 
            // Create a list to store nodes which will be added.  Optimization here is that we now know how many items are going to be added.
            _addedNodes = new List<XmlNode>(implicitImportCount);
        }
 
 
        /// <summary>
        /// Adds all implicit import nodes to the specified document.
        /// </summary>
        /// <param name="documentElement">The document element to add nodes to.</param>
        private void AddImplicitImportNodes(XmlElement documentElement)
        {
            List<ResolvedImport> implicitImports;
 
            // Do nothing if this project has no implicit imports
            if (!_implicitImportsByProject.TryGetValue(documentElement.OwnerDocument, out implicitImports))
            {
                return;
            }
 
            // Top implicit imports need to be added in the correct order by adding the first one at the top and each one after the first
            // one.  This variable keeps track of the last import that was added.
            XmlNode lastImplicitImportAdded = null;
 
            // Add the implicit top imports
            //
            foreach (ResolvedImport import in implicitImports.Where(i => i.ImportingElement.ImplicitImportLocation == ImplicitImportLocation.Top))
            {
                XmlElement xmlElement = (XmlElement)documentElement.OwnerDocument.ImportNode(import.ImportingElement.XmlElement, false);
                if (lastImplicitImportAdded == null)
                {
                    if (documentElement.FirstChild == null)
                    {
                        documentElement.AppendChild(xmlElement);
                    }
                    else
                    {
                        documentElement.InsertBefore(xmlElement, documentElement.FirstChild);
                    }
 
                    lastImplicitImportAdded = xmlElement;
                }
                else
                {
                    documentElement.InsertAfter(xmlElement, lastImplicitImportAdded);
                }
                _addedNodes.Add(xmlElement);
                AddToImportTable(xmlElement, import.ImportedProject);
            }
 
            // Add the implicit bottom imports
            //
            foreach (var import in implicitImports.Where(i => i.ImportingElement.ImplicitImportLocation == ImplicitImportLocation.Bottom))
            {
                XmlElement xmlElement = (XmlElement)documentElement.InsertAfter(documentElement.OwnerDocument.ImportNode(import.ImportingElement.XmlElement, false), documentElement.LastChild);
 
                _addedNodes.Add(xmlElement);
 
                AddToImportTable(xmlElement, import.ImportedProject);
            }
        }
 
        /// <summary>
        /// Recursively called method that clones source nodes into nodes in the destination
        /// document.
        /// </summary>
        private void CloneChildrenResolvingImports(XmlNode source, XmlNode destination)
        {
            XmlDocument sourceDocument = source.OwnerDocument ?? (XmlDocument)source;
            XmlDocument destinationDocument = destination.OwnerDocument ?? (XmlDocument)destination;
 
            foreach (XmlNode child in source.ChildNodes)
            {
                // Only one of <?xml version="1.0" encoding="utf-16"?> and we got it automatically already
                if (child.NodeType == XmlNodeType.XmlDeclaration)
                {
                    continue;
                }
 
                // If this is not the first <Project> tag
                if (
                    child.NodeType == XmlNodeType.Element &&
                    sourceDocument.DocumentElement == child &&                                      // This is the root element, not some random element named 'Project'
                    destinationDocument.DocumentElement != null &&                                  // Skip <Project> tag from the outer project
                    String.Equals(XMakeElements.project, child.Name, StringComparison.Ordinal))
                {
                    // But suffix any InitialTargets attribute
                    string outerInitialTargets = destinationDocument.DocumentElement.GetAttribute(XMakeAttributes.initialTargets).Trim();
                    string innerInitialTargets = ((XmlElement)child).GetAttribute(XMakeAttributes.initialTargets).Trim();
 
                    if (innerInitialTargets.Length > 0)
                    {
                        if (outerInitialTargets.Length > 0)
                        {
                            outerInitialTargets += ";";
                        }
 
                        destinationDocument.DocumentElement.SetAttribute(XMakeAttributes.initialTargets, outerInitialTargets + innerInitialTargets);
                    }
 
                    // Also gather any DefaultTargets value if none has been encountered already; put it on the outer <Project> tag
                    string outerDefaultTargets = destinationDocument.DocumentElement.GetAttribute(XMakeAttributes.defaultTargets).Trim();
 
                    if (outerDefaultTargets.Length == 0)
                    {
                        string innerDefaultTargets = ((XmlElement)child).GetAttribute(XMakeAttributes.defaultTargets).Trim();
 
                        if (innerDefaultTargets.Trim().Length > 0)
                        {
                            destinationDocument.DocumentElement.SetAttribute(XMakeAttributes.defaultTargets, innerDefaultTargets);
                        }
                    }
 
                    // Add any implicit imports for an imported document
                    AddImplicitImportNodes(child.OwnerDocument.DocumentElement);
 
                    CloneChildrenResolvingImports(child, destination);
                    continue;
                }
 
                // Resolve <Import> to 0-n documents and walk into them
                if (child.NodeType == XmlNodeType.Element && String.Equals(XMakeElements.import, child.Name, StringComparison.Ordinal))
                {
                    // To display what the <Import> tag looked like
                    string importCondition = ((XmlElement)child).GetAttribute(XMakeAttributes.condition);
                    string condition = importCondition.Length > 0 ? $" Condition=\"{importCondition}\"" : String.Empty;
                    string importProject = ((XmlElement)child).GetAttribute(XMakeAttributes.project).Replace("--", "__");
                    string importSdk = ((XmlElement)child).GetAttribute(XMakeAttributes.sdk);
                    string sdk = importSdk.Length > 0 ? $" {XMakeAttributes.sdk}=\"{importSdk}\"" : String.Empty;
 
                    // Get the Sdk attribute of the Project element if specified
                    string projectSdk = source.NodeType == XmlNodeType.Element && String.Equals(XMakeElements.project, source.Name, StringComparison.Ordinal) ? ((XmlElement)source).GetAttribute(XMakeAttributes.sdk) : String.Empty;
 
                    IList<ProjectRootElement> resolvedList;
                    if (!_importTable.TryGetValue((XmlElement)child, out resolvedList))
                    {
                        // Import didn't resolve to anything; just display as a comment and move on
                        string closedImportTag =
                            $"<Import Project=\"{importProject}\"{sdk}{condition} />";
                        destination.AppendChild(destinationDocument.CreateComment(closedImportTag));
 
                        continue;
                    }
 
                    for (int i = 0; i < resolvedList.Count; i++)
                    {
                        ProjectRootElement resolved = resolvedList[i];
                        XmlDocument innerDocument = resolved.XmlDocument;
 
                        string importTag =
                            $"  <Import Project=\"{importProject}\"{sdk}{condition}>";
 
                        if (!String.IsNullOrWhiteSpace(importSdk) && projectSdk.IndexOf(importSdk, StringComparison.OrdinalIgnoreCase) >= 0)
                        {
                            importTag +=
                                $"\r\n  This import was added implicitly because the {XMakeElements.project} element's {XMakeAttributes.sdk} attribute specified \"{importSdk}\".";
                        }
 
                        destination.AppendChild(destinationDocument.CreateComment(
                            $"\r\n{new String('=', 140)}\r\n{importTag}\r\n\r\n{resolved.FullPath.Replace("--", "__")}\r\n{new String('=', 140)}\r\n"));
 
                        _filePaths.Push(resolved.FullPath);
                        CloneChildrenResolvingImports(innerDocument, destination);
                        _filePaths.Pop();
 
                        if (i < resolvedList.Count - 1)
                        {
                            destination.AppendChild(destinationDocument.CreateComment("\r\n" + new String('=', 140) + "\r\n  </Import>\r\n" + new String('=', 140) + "\r\n"));
                        }
                        else
                        {
                            destination.AppendChild(destinationDocument.CreateComment("\r\n" + new String('=', 140) + "\r\n  </Import>\r\n\r\n" + _filePaths.Peek()?.Replace("--", "__") + "\r\n" + new String('=', 140) + "\r\n"));
                        }
                    }
 
                    continue;
                }
 
                // Skip over <ImportGroup> into its children
                if (child.NodeType == XmlNodeType.Element && String.Equals(XMakeElements.importGroup, child.Name, StringComparison.Ordinal))
                {
                    // To display what the <ImportGroup> tag looked like
                    string importGroupCondition = ((XmlElement)child).GetAttribute(XMakeAttributes.condition);
                    string importGroupTag = "<ImportGroup" + ((importGroupCondition.Length > 0) ? " Condition=\"" + importGroupCondition + "\"" : String.Empty) + ">";
                    destination.AppendChild(destinationDocument.CreateComment(importGroupTag));
 
                    CloneChildrenResolvingImports(child, destination);
 
                    destination.AppendChild(destinationDocument.CreateComment("</" + XMakeElements.importGroup + ">"));
 
                    continue;
                }
 
                // Node doesn't need special treatment, clone and append
                XmlNode clone = destinationDocument.ImportNode(child, false /* shallow */); // ImportNode does a clone but unlike CloneNode it works across XmlDocuments
 
                if (clone.NodeType == XmlNodeType.Element && String.Equals(XMakeElements.project, child.Name, StringComparison.Ordinal) && clone.Attributes?[XMakeAttributes.sdk] != null)
                {
                    clone.Attributes.Remove(clone.Attributes[XMakeAttributes.sdk]);
                }
 
                destination.AppendChild(clone);
 
                CloneChildrenResolvingImports(child, clone);
            }
        }
    }
}