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