1 // Copyright (c) Microsoft. All rights reserved.
2 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3 // </copyright>
4 // <summary>Helper to create a "preprocessed" or "logical" view of an evaluated Project.</summary>
5 //-----------------------------------------------------------------------
6 
7 using System;
8 using System.Collections.Generic;
9 using System.IO;
10 using System.Linq;
11 using System.Text;
12 using System.Xml;
13 using Microsoft.Build.Construction;
14 using Microsoft.Build.Framework;
15 using Microsoft.Build.Shared;
16 using Microsoft.Build.Internal;
17 using Microsoft.Build.Collections;
18 
19 namespace Microsoft.Build.Evaluation
20 {
21     /// <summary>
22     /// Creates a view of an evaluated project's XML as if it had all been loaded from
23     /// a single file, instead of being assembled by pulling in imported files as it actually was.
24     /// </summary>
25     /// <remarks>
26     /// Ideally the result would be buildable on its own, and *usually* this should be the case.
27     /// Known cases where it wouldn't be buildable:
28     /// -- $(MSBuildThisFile) and similar properties aren't corrected
29     /// -- relative path in exists(..) conditions is relative to the imported file
30     /// -- same for AssemblyFile on UsingTask
31     /// Paths in item includes are relative to the importing project, though.
32     /// </remarks>
33     internal class Preprocessor
34     {
35         /// <summary>
36         /// Project to preprocess
37         /// </summary>
38         private readonly Project _project;
39 
40         /// <summary>
41         /// Table to resolve import tags
42         /// </summary>
43         private readonly Dictionary<XmlElement, IList<ProjectRootElement>> _importTable;
44 
45         /// <summary>
46         /// Stack of file paths pushed as we follow imports
47         /// </summary>
48         private readonly Stack<string> _filePaths = new Stack<string>();
49 
50         /// <summary>
51         /// Used to keep track of nodes that were added to the document from implicit imports which will be removed later.
52         /// At the time of adding this feature, cloning is buggy so it is easier to just edit the DOM in memory.
53         /// </summary>
54         private List<XmlNode> _addedNodes;
55 
56         /// <summary>
57         /// Table of implicit imports by document.  The list per document contains both top and bottom imports.
58         /// </summary>
59         private readonly Dictionary<XmlDocument, List<ResolvedImport>> _implicitImportsByProject = new Dictionary<XmlDocument, List<ResolvedImport>>();
60 
61         /// <summary>
62         /// Constructor
63         /// </summary>
Preprocessor(Project project)64         private Preprocessor(Project project)
65         {
66             _project = project;
67 
68             IList<ResolvedImport> imports = project.Imports;
69 
70             _importTable = new Dictionary<XmlElement, IList<ProjectRootElement>>(imports.Count);
71 
72             foreach (ResolvedImport entry in imports)
73             {
74                 AddToImportTable(entry.ImportingElement.XmlElement, entry.ImportedProject);
75             }
76         }
77 
78         /// <summary>
79         /// Returns an XmlDocument representing the evaluated project's XML as if it all had
80         /// been loaded from a single file, instead of being assembled by pulling in imported files.
81         /// </summary>
GetPreprocessedDocument(Project project)82         internal static XmlDocument GetPreprocessedDocument(Project project)
83         {
84             Preprocessor preprocessor = new Preprocessor(project);
85 
86             XmlDocument result = preprocessor.Preprocess();
87 
88             return result;
89         }
90 
91         /// <summary>
92         /// Root of the preprocessing.
93         /// </summary>
Preprocess()94         private XmlDocument Preprocess()
95         {
96             XmlDocument outerDocument = _project.Xml.XmlDocument;
97 
98             CreateImplicitImportTable();
99 
100             AddImplicitImportNodes(outerDocument.DocumentElement);
101 
102             XmlDocument destinationDocument = (XmlDocument)outerDocument.CloneNode(false /* shallow */);
103 
104             _filePaths.Push(_project.FullPath);
105 
106             if (!String.IsNullOrEmpty(_project.FullPath)) // Ignore in-memory projects
107             {
108                 destinationDocument.AppendChild(destinationDocument.CreateComment("\r\n" + new String('=', 140) + "\r\n" + _project.FullPath.Replace("--", "__") + "\r\n" + new String('=', 140) + "\r\n"));
109             }
110 
111             CloneChildrenResolvingImports(outerDocument, destinationDocument);
112 
113             // Remove the nodes that were added as implicit imports
114             //
115             foreach (XmlNode node in _addedNodes)
116             {
117                 node.ParentNode?.RemoveChild(node);
118             }
119 
120             return destinationDocument;
121         }
122 
AddToImportTable(XmlElement element, ProjectRootElement importedProject)123         private void AddToImportTable(XmlElement element, ProjectRootElement importedProject)
124         {
125             IList<ProjectRootElement> list;
126             if (!_importTable.TryGetValue(element, out list))
127             {
128                 list = new List<ProjectRootElement>();
129                 _importTable[element] = list;
130             }
131 
132             list.Add(importedProject);
133         }
134 
135         /// <summary>
136         /// Creates a table containing implicit imports by project document.
137         /// </summary>
CreateImplicitImportTable()138         private void CreateImplicitImportTable()
139         {
140             int implicitImportCount = 0;
141 
142             // Loop through all implicit imports top and bottom
143             foreach (ResolvedImport resolvedImport in _project.Imports.Where(i => i.ImportingElement.ImplicitImportLocation != ImplicitImportLocation.None))
144             {
145                 implicitImportCount++;
146                 List<ResolvedImport> imports;
147 
148                 // Attempt to get an existing list from the dictionary
149                 if (!_implicitImportsByProject.TryGetValue(resolvedImport.ImportingElement.XmlDocument, out imports))
150                 {
151                     // Add a new list
152                     _implicitImportsByProject[resolvedImport.ImportingElement.XmlDocument] = new List<ResolvedImport>();
153 
154                     // Get a pointer to the list
155                     imports = _implicitImportsByProject[resolvedImport.ImportingElement.XmlDocument];
156                 }
157 
158                 imports.Add(resolvedImport);
159             }
160 
161             // 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.
162             _addedNodes = new List<XmlNode>(implicitImportCount);
163         }
164 
165 
166         /// <summary>
167         /// Adds all implicit import nodes to the specified document.
168         /// </summary>
169         /// <param name="documentElement">The document element to add nodes to.</param>
AddImplicitImportNodes(XmlElement documentElement)170         private void AddImplicitImportNodes(XmlElement documentElement)
171         {
172             List<ResolvedImport> implicitImports;
173 
174             // Do nothing if this project has no implicit imports
175             if (!_implicitImportsByProject.TryGetValue(documentElement.OwnerDocument, out implicitImports))
176             {
177                 return;
178             }
179 
180             // 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
181             // one.  This variable keeps track of the last import that was added.
182             XmlNode lastImplicitImportAdded = null;
183 
184             // Add the implicit top imports
185             //
186             foreach (ResolvedImport import in implicitImports.Where(i => i.ImportingElement.ImplicitImportLocation == ImplicitImportLocation.Top))
187             {
188                 XmlElement xmlElement = (XmlElement)documentElement.OwnerDocument.ImportNode(import.ImportingElement.XmlElement, false);
189                 if (lastImplicitImportAdded == null)
190                 {
191                     if (documentElement.FirstChild == null)
192                     {
193                         documentElement.AppendChild(xmlElement);
194                     }
195                     else
196                     {
197                         documentElement.InsertBefore(xmlElement, documentElement.FirstChild);
198                     }
199 
200                     lastImplicitImportAdded = xmlElement;
201                 }
202                 else
203                 {
204                     documentElement.InsertAfter(xmlElement, lastImplicitImportAdded);
205                 }
206                 _addedNodes.Add(xmlElement);
207                 AddToImportTable(xmlElement, import.ImportedProject);
208             }
209 
210             // Add the implicit bottom imports
211             //
212             foreach (var import in implicitImports.Where(i => i.ImportingElement.ImplicitImportLocation == ImplicitImportLocation.Bottom))
213             {
214                 XmlElement xmlElement = (XmlElement)documentElement.InsertAfter(documentElement.OwnerDocument.ImportNode(import.ImportingElement.XmlElement, false), documentElement.LastChild);
215 
216                 _addedNodes.Add(xmlElement);
217 
218                 AddToImportTable(xmlElement, import.ImportedProject);
219             }
220         }
221 
222         /// <summary>
223         /// Recursively called method that clones source nodes into nodes in the destination
224         /// document.
225         /// </summary>
CloneChildrenResolvingImports(XmlNode source, XmlNode destination)226         private void CloneChildrenResolvingImports(XmlNode source, XmlNode destination)
227         {
228             XmlDocument sourceDocument = source.OwnerDocument ?? (XmlDocument)source;
229             XmlDocument destinationDocument = destination.OwnerDocument ?? (XmlDocument)destination;
230 
231             foreach (XmlNode child in source.ChildNodes)
232             {
233                 // Only one of <?xml version="1.0" encoding="utf-16"?> and we got it automatically already
234                 if (child.NodeType == XmlNodeType.XmlDeclaration)
235                 {
236                     continue;
237                 }
238 
239                 // If this is not the first <Project> tag
240                 if (
241                     child.NodeType == XmlNodeType.Element &&
242                     sourceDocument.DocumentElement == child &&                                      // This is the root element, not some random element named 'Project'
243                     destinationDocument.DocumentElement != null &&                                  // Skip <Project> tag from the outer project
244                     String.Equals(XMakeElements.project, child.Name, StringComparison.Ordinal)
245                    )
246                 {
247                     // But suffix any InitialTargets attribute
248                     string outerInitialTargets = destinationDocument.DocumentElement.GetAttribute(XMakeAttributes.initialTargets).Trim();
249                     string innerInitialTargets = ((XmlElement)child).GetAttribute(XMakeAttributes.initialTargets).Trim();
250 
251                     if (innerInitialTargets.Length > 0)
252                     {
253                         if (outerInitialTargets.Length > 0)
254                         {
255                             outerInitialTargets += ";";
256                         }
257 
258                         destinationDocument.DocumentElement.SetAttribute(XMakeAttributes.initialTargets, outerInitialTargets + innerInitialTargets);
259                     }
260 
261                     // Also gather any DefaultTargets value if none has been encountered already; put it on the outer <Project> tag
262                     string outerDefaultTargets = destinationDocument.DocumentElement.GetAttribute(XMakeAttributes.defaultTargets).Trim();
263 
264                     if (outerDefaultTargets.Length == 0)
265                     {
266                         string innerDefaultTargets = ((XmlElement)child).GetAttribute(XMakeAttributes.defaultTargets).Trim();
267 
268                         if (innerDefaultTargets.Trim().Length > 0)
269                         {
270                             destinationDocument.DocumentElement.SetAttribute(XMakeAttributes.defaultTargets, innerDefaultTargets);
271                         }
272                     }
273 
274                     // Add any implicit imports for an imported document
275                     AddImplicitImportNodes(child.OwnerDocument.DocumentElement);
276 
277                     CloneChildrenResolvingImports(child, destination);
278                     continue;
279                 }
280 
281                 // Resolve <Import> to 0-n documents and walk into them
282                 if (child.NodeType == XmlNodeType.Element && String.Equals(XMakeElements.import, child.Name, StringComparison.Ordinal))
283                 {
284                     // To display what the <Import> tag looked like
285                     string importCondition = ((XmlElement)child).GetAttribute(XMakeAttributes.condition);
286                     string condition = importCondition.Length > 0 ? $" Condition=\"{importCondition}\"" : String.Empty;
287                     string importProject = ((XmlElement)child).GetAttribute(XMakeAttributes.project).Replace("--", "__");
288                     string importSdk = ((XmlElement)child).GetAttribute(XMakeAttributes.sdk);
289                     string sdk = importSdk.Length > 0 ? $" {XMakeAttributes.sdk}=\"{importSdk}\"" : String.Empty;
290 
291                     // Get the Sdk attribute of the Project element if specified
292                     string projectSdk = source.NodeType == XmlNodeType.Element && String.Equals(XMakeElements.project, source.Name, StringComparison.Ordinal) ? ((XmlElement) source).GetAttribute(XMakeAttributes.sdk) : String.Empty;
293 
294                     IList<ProjectRootElement> resolvedList;
295                     if (!_importTable.TryGetValue((XmlElement)child, out resolvedList))
296                     {
297                         // Import didn't resolve to anything; just display as a comment and move on
298                         string closedImportTag =
299                             $"<Import Project=\"{importProject}\"{sdk}{condition} />";
300                         destination.AppendChild(destinationDocument.CreateComment(closedImportTag));
301 
302                         continue;
303                     }
304 
305                     for (int i = 0; i < resolvedList.Count; i++)
306                     {
307                         ProjectRootElement resolved = resolvedList[i];
308                         XmlDocument innerDocument = resolved.XmlDocument;
309 
310                         string importTag =
311                             $"  <Import Project=\"{importProject}\"{sdk}{condition}>";
312 
313                         if (!String.IsNullOrWhiteSpace(importSdk) && projectSdk.IndexOf(importSdk, StringComparison.OrdinalIgnoreCase) >= 0)
314                         {
315                             importTag +=
316                                 $"\r\n  This import was added implicitly because the {XMakeElements.project} element's {XMakeAttributes.sdk} attribute specified \"{importSdk}\".";
317                         }
318 
319                         destination.AppendChild(destinationDocument.CreateComment(
320                             $"\r\n{new String('=', 140)}\r\n{importTag}\r\n\r\n{resolved.FullPath.Replace("--", "__")}\r\n{new String('=', 140)}\r\n"));
321 
322                         _filePaths.Push(resolved.FullPath);
323                         CloneChildrenResolvingImports(innerDocument, destination);
324                         _filePaths.Pop();
325 
326                         if (i < resolvedList.Count - 1)
327                         {
328                             destination.AppendChild(destinationDocument.CreateComment("\r\n" + new String('=', 140) + "\r\n  </Import>\r\n" + new String('=', 140) + "\r\n"));
329                         }
330                         else
331                         {
332                             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"));
333                         }
334                     }
335 
336                     continue;
337                 }
338 
339                 // Skip over <ImportGroup> into its children
340                 if (child.NodeType == XmlNodeType.Element && String.Equals(XMakeElements.importGroup, child.Name, StringComparison.Ordinal))
341                 {
342                     // To display what the <ImportGroup> tag looked like
343                     string importGroupCondition = ((XmlElement)child).GetAttribute(XMakeAttributes.condition);
344                     string importGroupTag = "<ImportGroup" + ((importGroupCondition.Length > 0) ? " Condition=\"" + importGroupCondition + "\"" : String.Empty) + ">";
345                     destination.AppendChild(destinationDocument.CreateComment(importGroupTag));
346 
347                     CloneChildrenResolvingImports(child, destination);
348 
349                     destination.AppendChild(destinationDocument.CreateComment("</" + XMakeElements.importGroup + ">"));
350 
351                     continue;
352                 }
353 
354                 // Node doesn't need special treatment, clone and append
355                 XmlNode clone = destinationDocument.ImportNode(child, false /* shallow */); // ImportNode does a clone but unlike CloneNode it works across XmlDocuments
356 
357                 if (clone.NodeType == XmlNodeType.Element && String.Equals(XMakeElements.project, child.Name, StringComparison.Ordinal) && clone.Attributes?[XMakeAttributes.sdk] != null)
358                 {
359                     clone.Attributes.Remove(clone.Attributes[XMakeAttributes.sdk]);
360                 }
361 
362                 destination.AppendChild(clone);
363 
364                 CloneChildrenResolvingImports(child, clone);
365             }
366         }
367     }
368 }
369