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 //-----------------------------------------------------------------------
4 // </copyright>
5 // <summary>Definition of ProjectRootElement class.</summary>
6 //-----------------------------------------------------------------------
7 
8 using System;
9 using System.Collections.Generic;
10 using System.Diagnostics;
11 using System.Globalization;
12 using System.IO;
13 using System.Linq;
14 using System.Runtime.CompilerServices;
15 using System.Text;
16 using System.Text.RegularExpressions;
17 using System.Threading;
18 using System.Xml;
19 using Microsoft.Build.BackEnd.Logging;
20 using Microsoft.Build.Collections;
21 using Microsoft.Build.Evaluation;
22 using Microsoft.Build.Framework;
23 using Microsoft.Build.Shared;
24 using Microsoft.Build.Internal;
25 #if (!STANDALONEBUILD)
26 using Microsoft.Internal.Performance;
27 #if MSBUILDENABLEVSPROFILING
28 using Microsoft.VisualStudio.Profiler;
29 #endif
30 #endif
31 using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService;
32 using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities;
33 using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException;
34 
35 namespace Microsoft.Build.Construction
36 {
37     /// <summary>
38     /// Event handler for the event fired after this project file is named or renamed.
39     /// If the project file has not previously had a name, oldFullPath is null.
40     /// </summary>
RenameHandlerDelegate(string oldFullPath)41     internal delegate void RenameHandlerDelegate(string oldFullPath);
42 
43     /// <summary>
44     /// ProjectRootElement class represents an MSBuild project, an MSBuild targets file or any other file that conforms to MSBuild
45     /// project file schema.
46     /// This class and its related classes allow a complete MSBuild project or targets file to be read and written.
47     /// Comments and whitespace cannot be edited through this model at present.
48     ///
49     /// Each project root element is associated with exactly one ProjectCollection. This allows the owner of that project collection
50     /// to control its lifetime and not be surprised by edits via another project collection.
51     /// </summary>
52     [DebuggerDisplay("{FullPath} #Children={Count} DefaultTargets={DefaultTargets} ToolsVersion={ToolsVersion} InitialTargets={InitialTargets} ExplicitlyLoaded={IsExplicitlyLoaded}")]
53     public class ProjectRootElement : ProjectElementContainer
54     {
55         /// Constants for default (empty) project file.
56         private const string EmptyProjectFileContent = "{0}<Project{1}{2}>\r\n</Project>";
57         private const string EmptyProjectFileXmlDeclaration = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
58         private const string EmptyProjectFileToolsVersion = " ToolsVersion=\"" + MSBuildConstants.CurrentToolsVersion + "\"";
59         private const string EmptyProjectFileXmlNamespace = " xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\"";
60 
61         /// <summary>
62         /// The singleton delegate that loads projects into the ProjectRootElement
63         /// </summary>
64         private static readonly ProjectRootElementCache.OpenProjectRootElement s_openLoaderDelegate = OpenLoader;
65 
66         private static readonly ProjectRootElementCache.OpenProjectRootElement s_openLoaderPreserveFormattingDelegate = OpenLoaderPreserveFormatting;
67 
68         /// <summary>
69         /// Used to determine if a file is an empty XML file if it ONLY contains an XML declaration like &lt;?xml version="1.0" encoding="utf-8"?&gt;.
70         /// </summary>
71         private static readonly Lazy<Regex> XmlDeclarationRegEx = new Lazy<Regex>(() => new Regex(@"\A\s*\<\?\s*xml.*\?\>\s*\Z"), isThreadSafe: true);
72 
73         /// <summary>
74         /// The default encoding to use / assume for a new project.
75         /// </summary>
76         private static readonly Encoding s_defaultEncoding = Encoding.UTF8;
77 
78         /// <summary>
79         /// A global counter used to ensure each project version is distinct from every other.
80         /// </summary>
81         /// <remarks>
82         /// This number is static so that it is unique across the appdomain. That is so that a host
83         /// can know when a ProjectRootElement has been unloaded (perhaps after modification) and
84         /// reloaded -- the version won't reset to '0'.
85         /// </remarks>
86         private static int s_globalVersionCounter;
87 
88         /// <summary>
89         /// Version number of this object that was last saved to disk, or last loaded from disk.
90         /// Used to figure whether this object is dirty for saving.
91         /// Saving to or loading from a provided stream reader does not modify this value, only saving to or loading from disk.
92         /// The actual value is meaningless (since the counter is shared with all projects) --
93         /// it should only be compared to a stored value.
94         /// Immediately after loading from disk, this has the same value as <see cref="_version">version</see>.
95         /// </summary>
96         private int _versionOnDisk;
97 
98         /// <summary>
99         /// Current version number of this object.
100         /// Used to figure whether this object is dirty for saving, or projects evaluated from
101         /// this object need to be re-evaluated.
102         /// The actual value is meaningless (since the counter is shared with all projects) --
103         /// it should only be compared to a stored value.
104         /// </summary>
105         /// <remarks>
106         /// Set this only through <see cref="MarkDirty(string, string)"/>.
107         /// </remarks>
108         private int _version;
109 
110         /// <summary>
111         /// The encoding of the project that was (if applicable) loaded off disk, and that will be used to save the project.
112         /// </summary>
113         /// <value>Defaults to UTF8 for new projects.</value>
114         private Encoding _encoding;
115 
116         /// <summary>
117         /// XML namespace specified and used by this project file. If a namespace was not specified in the project file, this
118         /// value will be string.Empty.
119         /// </summary>
120         internal string XmlNamespace { get; set; }
121 
122         /// <summary>
123         /// The project file's location. It can be null if the project is not directly loaded from a file.
124         /// </summary>
125         private ElementLocation _projectFileLocation;
126 
127         /// <summary>
128         /// The directory that the project is in.
129         /// Essential for evaluating relative paths.
130         /// If the project is not loaded from disk, returns the current-directory from
131         /// the time the project was loaded - this is the same behavior as Whidbey/Orcas.
132         /// </summary>
133         private string _directory;
134 
135         /// <summary>
136         /// The time that this object was last changed. If it hasn't
137         /// been changed since being loaded or created, its value is <see cref="DateTime.MinValue"/>.
138         /// Stored as UTC as this is faster when there are a large number of rapid edits.
139         /// </summary>
140         private DateTime _timeLastChangedUtc;
141 
142         /// <summary>
143         /// The last-write-time of the file that was read, when it was read.
144         /// This can be used to see whether the file has been changed on disk
145         /// by an external means.
146         /// </summary>
147         private DateTime _lastWriteTimeWhenRead;
148 
149         /// <summary>
150         /// The cache in which this project root element is stored.
151         /// </summary>
152         private readonly ProjectRootElementCache _projectRootElementCache;
153 
154         /// <summary>
155         /// Reason it was last marked dirty; unlocalized, for debugging
156         /// </summary>
157         private string _dirtyReason = "first created project {0}";
158 
159         /// <summary>
160         /// Parameter to be formatted into the dirty reason
161         /// </summary>
162         private string _dirtyParameter = String.Empty;
163 
164         /// <summary>
165         /// Initialize a ProjectRootElement instance from a XmlReader.
166         /// May throw InvalidProjectFileException.
167         /// Leaves the project dirty, indicating there are unsaved changes.
168         /// Used to create a root element for solutions loaded by the 3.5 version of the solution wrapper.
169         /// </summary>
ProjectRootElement(XmlReader xmlReader, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded, bool preserveFormatting)170         internal ProjectRootElement(XmlReader xmlReader, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded,
171             bool preserveFormatting)
172             : base()
173         {
174             ErrorUtilities.VerifyThrowArgumentNull(xmlReader, "xmlReader");
175             ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache, "projectRootElementCache");
176 
177             this.IsExplicitlyLoaded = isExplicitlyLoaded;
178             _projectRootElementCache = projectRootElementCache;
179             _directory = NativeMethodsShared.GetCurrentDirectory();
180             IncrementVersion();
181 
182             XmlDocumentWithLocation document = LoadDocument(xmlReader, preserveFormatting);
183 
184             ProjectParser.Parse(document, this);
185         }
186 
187         /// <summary>
188         /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
189         /// Leaves the project dirty, indicating there are unsaved changes.
190         /// </summary>
ProjectRootElement(ProjectRootElementCache projectRootElementCache, NewProjectFileOptions projectFileOptions)191         private ProjectRootElement(ProjectRootElementCache projectRootElementCache, NewProjectFileOptions projectFileOptions)
192         {
193             ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache, "projectRootElementCache");
194 
195             _projectRootElementCache = projectRootElementCache;
196             _directory = NativeMethodsShared.GetCurrentDirectory();
197             IncrementVersion();
198 
199             XmlDocumentWithLocation document = new XmlDocumentWithLocation();
200 
201             XmlReaderSettings xrs = new XmlReaderSettings();
202             xrs.DtdProcessing = DtdProcessing.Ignore;
203 
204             var emptyProjectFile = string.Format(EmptyProjectFileContent,
205                 (projectFileOptions & NewProjectFileOptions.IncludeXmlDeclaration) != 0 ? EmptyProjectFileXmlDeclaration : string.Empty,
206                 (projectFileOptions & NewProjectFileOptions.IncludeToolsVersion) != 0 ? EmptyProjectFileToolsVersion : string.Empty,
207                 (projectFileOptions & NewProjectFileOptions.IncludeXmlNamespace) != 0 ? EmptyProjectFileXmlNamespace : string.Empty);
208 
209             using (XmlReader xr = XmlReader.Create(new StringReader(emptyProjectFile), xrs))
210             {
211                 document.Load(xr);
212             }
213 
214             ProjectParser.Parse(document, this);
215         }
216 
217         /// <summary>
218         /// Initialize a ProjectRootElement instance over a project with the specified file path.
219         /// Assumes path is already normalized.
220         /// May throw InvalidProjectFileException.
221         /// </summary>
ProjectRootElement(string path, ProjectRootElementCache projectRootElementCache, bool preserveFormatting)222         private ProjectRootElement(string path, ProjectRootElementCache projectRootElementCache,
223             bool preserveFormatting)
224             : base()
225         {
226             ErrorUtilities.VerifyThrowArgumentLength(path, "path");
227             ErrorUtilities.VerifyThrowInternalRooted(path);
228             ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache, "projectRootElementCache");
229             _projectRootElementCache = projectRootElementCache;
230 
231             IncrementVersion();
232             _versionOnDisk = _version;
233             _timeLastChangedUtc = DateTime.UtcNow;
234 
235             XmlDocumentWithLocation document = LoadDocument(path, preserveFormatting);
236 
237             ProjectParser.Parse(document, this);
238 
239             projectRootElementCache.AddEntry(this);
240         }
241 
242         /// <summary>
243         /// Initialize a ProjectRootElement instance from an existing document.
244         /// May throw InvalidProjectFileException.
245         /// Leaves the project dirty, indicating there are unsaved changes.
246         /// </summary>
247         /// <remarks>
248         /// Do not make public: we do not wish to expose particular XML API's.
249         /// </remarks>
ProjectRootElement(XmlDocumentWithLocation document, ProjectRootElementCache projectRootElementCache)250         private ProjectRootElement(XmlDocumentWithLocation document, ProjectRootElementCache projectRootElementCache)
251             : base()
252         {
253             ErrorUtilities.VerifyThrowArgumentNull(document, "document");
254             ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache, "projectRootElementCache");
255 
256             _projectRootElementCache = projectRootElementCache;
257             _directory = NativeMethodsShared.GetCurrentDirectory();
258             IncrementVersion();
259 
260             ProjectParser.Parse(document, this);
261         }
262 
263         /// <summary>
264         /// Initialize a ProjectRootElement instance from an existing document.
265         /// Helper constructor for the <see cref="ReloadFrom(string,bool,System.Nullable{bool})"/>> mehtod which needs to check if the document parses
266         /// </summary>
267         /// <remarks>
268         /// Do not make public: we do not wish to expose particular XML API's.
269         /// </remarks>
ProjectRootElement(XmlDocumentWithLocation document)270         private ProjectRootElement(XmlDocumentWithLocation document)
271             : base()
272         {
273             ProjectParser.Parse(document, this);
274         }
275 
276         /// <summary>
277         /// Event raised after this project is renamed
278         /// </summary>
279         internal event RenameHandlerDelegate OnAfterProjectRename;
280 
281         /// <summary>
282         /// Event raised after the project XML is changed.
283         /// </summary>
284         internal event EventHandler<ProjectXmlChangedEventArgs> OnProjectXmlChanged;
285 
286         /// <summary>
287         /// Condition should never be set, but the getter returns null instead of throwing
288         /// because a nonexistent condition is implicitly true
289         /// </summary>
290         public override string Condition
291         {
292             get
293             {
294                 return null;
295             }
296 
297             set
298             {
299                 ErrorUtilities.ThrowInvalidOperation("OM_CannotGetSetCondition");
300             }
301         }
302 
303         #region ChildEnumerators
304         /// <summary>
305         /// Get a read-only collection of the child chooses, if any
306         /// </summary>
307         /// <remarks>
308         /// The name is inconsistent to make it more understandable, per API review.
309         /// </remarks>
310         public ICollection<ProjectChooseElement> ChooseElements => new ReadOnlyCollection<ProjectChooseElement>
311         (
312             new FilteringEnumerable<ProjectElement, ProjectChooseElement>(Children)
313         );
314 
315         /// <summary>
316         /// Get a read-only collection of the child item definition groups, if any
317         /// </summary>
318         public ICollection<ProjectItemDefinitionGroupElement> ItemDefinitionGroups => new ReadOnlyCollection<ProjectItemDefinitionGroupElement>
319         (
320             new FilteringEnumerable<ProjectElement, ProjectItemDefinitionGroupElement>(Children)
321         );
322 
323         /// <summary>
324         /// Get a read-only collection of the child item definitions, if any, in all item definition groups anywhere in the project file.
325         /// </summary>
326         public ICollection<ProjectItemDefinitionElement> ItemDefinitions => new ReadOnlyCollection<ProjectItemDefinitionElement>
327         (
328             new FilteringEnumerable<ProjectElement, ProjectItemDefinitionElement>(AllChildren)
329         );
330 
331         /// <summary>
332         /// Get a read-only collection over the child item groups, if any.
333         /// Does not include any that may not be at the root, i.e. inside Choose elements.
334         /// </summary>
335         public ICollection<ProjectItemGroupElement> ItemGroups => new ReadOnlyCollection<ProjectItemGroupElement>
336         (
337             new FilteringEnumerable<ProjectElement, ProjectItemGroupElement>(Children)
338         );
339 
340         /// <summary>
341         /// Get a read-only collection of the child items, if any, in all item groups anywhere in the project file.
342         /// Not restricted to root item groups: traverses through Choose elements.
343         /// </summary>
344         public ICollection<ProjectItemElement> Items => new ReadOnlyCollection<ProjectItemElement>
345         (
346             new FilteringEnumerable<ProjectElement, ProjectItemElement>(AllChildren)
347         );
348 
349         /// <summary>
350         /// Get a read-only collection of the child import groups, if any.
351         /// </summary>
352         public ICollection<ProjectImportGroupElement> ImportGroups => new ReadOnlyCollection<ProjectImportGroupElement>
353         (
354             new FilteringEnumerable<ProjectElement, ProjectImportGroupElement>(Children)
355         );
356 
357         /// <summary>
358         /// Get a read-only collection of the child imports
359         /// </summary>
360         public ICollection<ProjectImportElement> Imports => new ReadOnlyCollection<ProjectImportElement>
361         (
362             new FilteringEnumerable<ProjectElement, ProjectImportElement>(AllChildren)
363         );
364 
365         /// <summary>
366         /// Get a read-only collection of the child property groups, if any.
367         /// Does not include any that may not be at the root, i.e. inside Choose elements.
368         /// </summary>
369         public ICollection<ProjectPropertyGroupElement> PropertyGroups => new ReadOnlyCollection<ProjectPropertyGroupElement>
370         (
371             new FilteringEnumerable<ProjectElement, ProjectPropertyGroupElement>(Children)
372         );
373 
374         /// <summary>
375         /// Geta read-only collection of the child properties, if any, in all property groups anywhere in the project file.
376         /// Not restricted to root property groups: traverses through Choose elements.
377         /// </summary>
378         public ICollection<ProjectPropertyElement> Properties => new ReadOnlyCollection<ProjectPropertyElement>
379         (
380             new FilteringEnumerable<ProjectElement, ProjectPropertyElement>(AllChildren)
381         );
382 
383         /// <summary>
384         /// Get a read-only collection of the child targets
385         /// </summary>
386         public ICollection<ProjectTargetElement> Targets => new ReadOnlyCollection<ProjectTargetElement>
387         (
388             new FilteringEnumerable<ProjectElement, ProjectTargetElement>(Children)
389         );
390 
391         /// <summary>
392         /// Get a read-only collection of the child usingtasks, if any
393         /// </summary>
394         public ICollection<ProjectUsingTaskElement> UsingTasks => new ReadOnlyCollection<ProjectUsingTaskElement>
395         (
396             new FilteringEnumerable<ProjectElement, ProjectUsingTaskElement>(Children)
397         );
398 
399         /// <summary>
400         /// Get a read-only collection of the child item groups, if any, in reverse order
401         /// </summary>
402         public ICollection<ProjectItemGroupElement> ItemGroupsReversed => new ReadOnlyCollection<ProjectItemGroupElement>
403         (
404             new FilteringEnumerable<ProjectElement, ProjectItemGroupElement>(ChildrenReversed)
405         );
406 
407         /// <summary>
408         /// Get a read-only collection of the child item definition groups, if any, in reverse order
409         /// </summary>
410         public ICollection<ProjectItemDefinitionGroupElement> ItemDefinitionGroupsReversed => new ReadOnlyCollection<ProjectItemDefinitionGroupElement>
411         (
412             new FilteringEnumerable<ProjectElement, ProjectItemDefinitionGroupElement>(ChildrenReversed)
413         );
414 
415         /// <summary>
416         /// Get a read-only collection of the child import groups, if any, in reverse order
417         /// </summary>
418         public ICollection<ProjectImportGroupElement> ImportGroupsReversed => new ReadOnlyCollection<ProjectImportGroupElement>
419         (
420             new FilteringEnumerable<ProjectElement, ProjectImportGroupElement>(ChildrenReversed)
421         );
422 
423         /// <summary>
424         /// Get a read-only collection of the child property groups, if any, in reverse order
425         /// </summary>
426         public ICollection<ProjectPropertyGroupElement> PropertyGroupsReversed => new ReadOnlyCollection<ProjectPropertyGroupElement>
427         (
428             new FilteringEnumerable<ProjectElement, ProjectPropertyGroupElement>(ChildrenReversed)
429         );
430 
431         #endregion
432 
433         /// <summary>
434         /// The directory that the project is in.
435         /// Essential for evaluating relative paths.
436         /// Is never null, even if the FullPath does not contain directory information.
437         /// If the project has not been loaded from disk and has not been given a path, returns the current-directory from
438         /// the time the project was loaded - this is the same behavior as Whidbey/Orcas.
439         /// If the project has not been loaded from disk but has been given a path, this path may not exist.
440         /// </summary>
441         public string DirectoryPath
442         {
443             [DebuggerStepThrough] get { return _directory ?? String.Empty; }
444             internal set { _directory = value; }
445             // Used during solution load to ensure solutions which were created from a file have a location.
446         }
447 
448         /// <summary>
449         /// Full path to the project file.
450         /// If the project has not been loaded from disk and has not been given a path, returns null.
451         /// If the project has not been loaded from disk but has been given a path, this path may not exist.
452         /// Setter renames the project, if it already had a name.
453         /// </summary>
454         /// <remarks>
455         /// Updates the ProjectRootElement cache.
456         /// </remarks>
457         public string FullPath
458         {
459             get
460             {
461                 return _projectFileLocation?.File;
462             }
463 
464             set
465             {
466                 ErrorUtilities.VerifyThrowArgumentLength(value, "value");
467 
468                 string oldFullPath = _projectFileLocation?.File;
469 
470                 // We do not control the current directory at this point, but assume that if we were
471                 // passed a relative path, the caller assumes we will prepend the current directory.
472                 string newFullPath = FileUtilities.NormalizePath(value);
473 
474                 if (String.Equals(oldFullPath, newFullPath, StringComparison.OrdinalIgnoreCase))
475                 {
476                     return;
477                 }
478 
479                 _projectFileLocation = ElementLocation.Create(newFullPath);
480                 _directory = Path.GetDirectoryName(newFullPath);
481 
482                 if (XmlDocument != null)
483                 {
484                     XmlDocument.FullPath = newFullPath;
485                 }
486 
487                 if (oldFullPath == null)
488                 {
489                     _projectRootElementCache.AddEntry(this);
490                 }
491                 else
492                 {
493                     _projectRootElementCache.RenameEntry(oldFullPath, this);
494                 }
495 
496                 OnAfterProjectRename?.Invoke(oldFullPath);
497 
498                 MarkDirty("Set project FullPath to '{0}'", FullPath);
499             }
500         }
501 
502         /// <summary>
503         /// Encoding that the project file is saved in, or will be saved in, unless
504         /// otherwise specified.
505         /// </summary>
506         /// <remarks>
507         /// Returns the encoding from the Xml declaration if any, otherwise UTF8.
508         /// </remarks>
509         public Encoding Encoding
510         {
511             get
512             {
513                 // No thread-safety lock required here because many reader threads would set the same value to the field.
514                 if (_encoding == null)
515                 {
516                     XmlDeclaration declaration = XmlDocument.FirstChild as XmlDeclaration;
517 
518                     if (declaration?.Encoding.Length > 0)
519                     {
520                         _encoding = Encoding.GetEncoding(declaration.Encoding);
521                     }
522                 }
523 
524                 // Ensure we never return null, in case there was no xml declaration that we could find above.
525                 return _encoding ?? s_defaultEncoding;
526             }
527         }
528 
529         /// <summary>
530         /// Gets or sets the value of DefaultTargets. If there is no DefaultTargets, returns empty string.
531         /// If the value is null or empty, removes the attribute.
532         /// </summary>
533         public string DefaultTargets
534         {
535             [DebuggerStepThrough]
536             get
537             {
538                 return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.defaultTargets);
539             }
540 
541             [DebuggerStepThrough]
542             set
543             {
544                 ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.defaultTargets, value);
545                 MarkDirty("Set Project DefaultTargets to '{0}'", value);
546             }
547         }
548 
549         /// <summary>
550         /// Gets or sets the value of InitialTargets. If there is no InitialTargets, returns empty string.
551         /// If the value is null or empty, removes the attribute.
552         /// </summary>
553         public string InitialTargets
554         {
555             [DebuggerStepThrough]
556             get
557             {
558                 return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.initialTargets);
559             }
560 
561             [DebuggerStepThrough]
562             set
563             {
564                 ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.initialTargets, value);
565                 MarkDirty("Set project InitialTargets to '{0}'", value);
566             }
567         }
568 
569         /// <summary>
570         /// Gets or sets a semicolon delimited list of software development kits (SDK) that the project uses.
571         /// If  a value is specified, an Sdk.props is simplicity imported at the top of the project and an
572         /// Sdk.targets is simplicity imported at the bottom from the specified SDK.
573         /// If the value is null or empty, removes the attribute.
574         /// </summary>
575         public string Sdk
576         {
577             [DebuggerStepThrough]
578             get
579             {
580                 return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.sdk);
581             }
582 
583             [DebuggerStepThrough]
584             set
585             {
586                 ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.sdk, value);
587                 MarkDirty("Set project Sdk to '{0}'", value);
588             }
589         }
590 
591         /// <summary>
592         /// Gets or sets the value of TreatAsLocalProperty. If there is no tag, returns empty string.
593         /// If the value being set is null or empty, removes the attribute.
594         /// </summary>
595         public string TreatAsLocalProperty
596         {
597             [DebuggerStepThrough]
598             get
599             {
600                 return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.treatAsLocalProperty);
601             }
602 
603             [DebuggerStepThrough]
604             set
605             {
606                 ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.treatAsLocalProperty, value);
607                 MarkDirty("Set project TreatAsLocalProperty to '{0}'", value);
608             }
609         }
610 
611         /// <summary>
612         /// Gets or sets the value of ToolsVersion. If there is no ToolsVersion, returns empty string.
613         /// If the value is null or empty, removes the attribute.
614         /// </summary>
615         public string ToolsVersion
616         {
617             [DebuggerStepThrough]
618             get
619             {
620                 return ProjectXmlUtilities.GetAttributeValue(XmlElement, XMakeAttributes.toolsVersion);
621             }
622 
623             [DebuggerStepThrough]
624             set
625             {
626                 ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, XMakeAttributes.toolsVersion, value);
627                 MarkDirty("Set project ToolsVersion {0}", value);
628             }
629         }
630 
631         /// <summary>
632         /// Gets the XML representing this project as a string.
633         /// Does not remove any dirty flag.
634         /// </summary>
635         /// <remarks>
636         /// Useful for debugging.
637         /// Note that we do not expose an XmlDocument or any other specific XML API.
638         /// </remarks>
639         public string RawXml
640         {
641             get
642             {
643                 using (StringWriter stringWriter = new EncodingStringWriter(Encoding))
644                 {
645                     using (ProjectWriter projectWriter = new ProjectWriter(stringWriter))
646                     {
647                         projectWriter.Initialize(XmlDocument);
648                         XmlDocument.Save(projectWriter);
649                     }
650 
651                     return stringWriter.ToString();
652                 }
653             }
654         }
655 
656         /// <summary>
657         /// Whether the XML has been modified since it was last loaded or saved.
658         /// </summary>
659         public bool HasUnsavedChanges => Version != _versionOnDisk;
660 
661         /// <summary>
662         /// Whether the XML is preserving formatting or not.
663         /// </summary>
664         public bool PreserveFormatting => XmlDocument?.PreserveWhitespace ?? false;
665 
666         /// <summary>
667         /// Version number of this object.
668         /// A host can compare this to a stored version number to determine whether
669         /// a project's XML has changed, even if it has also been saved since.
670         ///
671         /// The actual value is meaningless: an edit may increment it more than once,
672         /// so it should only be compared to a stored value.
673         /// </summary>
674         /// <remarks>
675         /// Used by the Project class to figure whether changes have occurred that
676         /// it might want to pick up by reevaluation.
677         ///
678         /// Used by the ProjectRootElement class to determine whether it needs to save.
679         ///
680         /// This number is unique to the appdomain. That means that it is possible
681         /// to know when a ProjectRootElement has been unloaded (perhaps after modification) and
682         /// reloaded -- the version won't reset to '0'.
683         ///
684         /// We're assuming we don't have over 2 billion edits.
685         /// </remarks>
686         public int Version => _version;
687 
688         /// <summary>
689         /// The time that this object was last changed. If it hasn't
690         /// been changed since being loaded or created, its value is <see cref="DateTime.MinValue"/>.
691         /// </summary>
692         /// <remarks>
693         /// This is used by the VB/C# project system.
694         /// </remarks>
695         public DateTime TimeLastChanged => _timeLastChangedUtc.ToLocalTime();
696 
697         /// <summary>
698         /// The last-write-time of the file that was read, when it was read.
699         /// This can be used to see whether the file has been changed on disk
700         /// by an external means.
701         /// </summary>
702         public DateTime LastWriteTimeWhenRead => _lastWriteTimeWhenRead;
703 
704         /// <summary>
705         /// This does not allow conditions, so it should not be called.
706         /// </summary>
707         public override ElementLocation ConditionLocation
708         {
709             get
710             {
711                 ErrorUtilities.ThrowInternalError("Should not evaluate this");
712                 return null;
713             }
714         }
715 
716         /// <summary>
717         /// Location of the originating file itself, not any specific content within it.
718         /// If the file has not been given a name, returns an empty location.
719         /// This is a case where it is legitimate to "not have a location".
720         /// </summary>
721         public ElementLocation ProjectFileLocation => _projectFileLocation ?? ElementLocation.EmptyLocation;
722 
723         /// <summary>
724         /// Location of the toolsversion attribute, if any
725         /// </summary>
726         public ElementLocation ToolsVersionLocation => XmlElement.GetAttributeLocation(XMakeAttributes.toolsVersion);
727 
728         /// <summary>
729         /// Location of the defaulttargets attribute, if any
730         /// </summary>
731         public ElementLocation DefaultTargetsLocation => XmlElement.GetAttributeLocation(XMakeAttributes.defaultTargets);
732 
733         /// <summary>
734         /// Location of the initialtargets attribute, if any
735         /// </summary>
736         public ElementLocation InitialTargetsLocation => XmlElement.GetAttributeLocation(XMakeAttributes.initialTargets);
737 
738         /// <summary>
739         /// Location of the Sdk attribute, if any
740         /// </summary>
741         public ElementLocation SdkLocation => XmlElement.GetAttributeLocation(XMakeAttributes.sdk);
742 
743         /// <summary>
744         /// Location of the TreatAsLocalProperty attribute, if any
745         /// </summary>
746         public ElementLocation TreatAsLocalPropertyLocation
747             => XmlElement.GetAttributeLocation(XMakeAttributes.treatAsLocalProperty);
748 
749         /// <summary>
750         /// Has the project root element been explicitly loaded for a build or has it been implicitly loaded
751         /// as part of building another project.
752         /// </summary>
753         /// <remarks>
754         /// Internal code that wants to set this to true should call <see cref="MarkAsExplicitlyLoaded"/>.
755         /// The setter is private to make it more difficult to downgrade an existing PRE to an implicitly loaded state, which should never happen.
756         /// </remarks>
757         internal bool IsExplicitlyLoaded
758         {
759             get;
760             private set;
761         }
762 
763         /// <summary>
764         /// Retrieves the root element cache with which this root element is associated.
765         /// </summary>
766         internal ProjectRootElementCache ProjectRootElementCache => _projectRootElementCache;
767 
768         /// <summary>
769         /// Gets a value indicating whether this PRE is known by its containing collection.
770         /// </summary>
771         internal bool IsMemberOfProjectCollection => _projectFileLocation != null;
772 
773         /// <summary>
774         /// Indicates whether there are any targets in this project
775         /// that use the "Returns" attribute.  If so, then this project file
776         /// is automatically assumed to be "Returns-enabled", and the default behavior
777         /// for targets without Returns attributes changes from using the Outputs to
778         /// returning nothing by default.
779         /// </summary>
780         internal bool ContainsTargetsWithReturnsAttribute
781         {
782             get;
783             set;
784         }
785 
786         /// <summary>
787         /// Gets the ProjectExtensions child, if any, otherwise null.
788         /// </summary>
789         /// <remarks>
790         /// Not public as we do not wish to encourage the use of ProjectExtensions.
791         /// </remarks>
792         internal ProjectExtensionsElement ProjectExtensions
793             => ChildrenReversed.OfType<ProjectExtensionsElement>().FirstOrDefault();
794 
795         /// <summary>
796         /// Returns an unlocalized indication of how this file was last dirtied.
797         /// This is for debugging purposes only.
798         /// String formatting only occurs when retrieved.
799         /// </summary>
800         internal string LastDirtyReason
801             => _dirtyReason == null ? null : String.Format(CultureInfo.InvariantCulture, _dirtyReason, _dirtyParameter);
802 
803         /// <summary>
804         /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
805         /// Uses the global project collection.
806         /// </summary>
Create()807         public static ProjectRootElement Create()
808         {
809             return Create(ProjectCollection.GlobalProjectCollection, Project.DefaultNewProjectTemplateOptions);
810         }
811 
812         /// <summary>
813         /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later using the specified <see cref="NewProjectFileOptions"/>.
814         /// Uses the global project collection.
815         /// </summary>
Create(NewProjectFileOptions projectFileOptions)816         public static ProjectRootElement Create(NewProjectFileOptions projectFileOptions)
817         {
818             return Create(ProjectCollection.GlobalProjectCollection, projectFileOptions);
819         }
820 
821         /// <summary>
822         /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
823         /// Uses the specified project collection.
824         /// </summary>
Create(ProjectCollection projectCollection)825         public static ProjectRootElement Create(ProjectCollection projectCollection)
826         {
827             return Create(projectCollection.ProjectRootElementCache);
828         }
829 
830         /// <summary>
831         /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later using the specified <see cref="ProjectCollection"/> and <see cref="NewProjectFileOptions"/>.
832         /// </summary>
Create(ProjectCollection projectCollection, NewProjectFileOptions projectFileOptions)833         public static ProjectRootElement Create(ProjectCollection projectCollection, NewProjectFileOptions projectFileOptions)
834         {
835             ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection");
836 
837             return Create(projectCollection.ProjectRootElementCache, projectFileOptions);
838         }
839 
840 
841         /// <summary>
842         /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
843         /// Uses the global project collection.
844         /// </summary>
Create(string path)845         public static ProjectRootElement Create(string path)
846         {
847             return Create(path, ProjectCollection.GlobalProjectCollection, Project.DefaultNewProjectTemplateOptions);
848         }
849 
850         /// <summary>
851         /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later using the specified path and <see cref="NewProjectFileOptions"/>.
852         /// Uses the global project collection.
853         /// </summary>
Create(string path, NewProjectFileOptions newProjectFileOptions)854         public static ProjectRootElement Create(string path, NewProjectFileOptions newProjectFileOptions)
855         {
856             return Create(path, ProjectCollection.GlobalProjectCollection, newProjectFileOptions);
857         }
858 
859         /// <summary>
860         /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
861         /// Uses the specified project collection.
862         /// </summary>
Create(string path, ProjectCollection projectCollection)863         public static ProjectRootElement Create(string path, ProjectCollection projectCollection)
864         {
865             return Create(path, projectCollection, Project.DefaultNewProjectTemplateOptions);
866         }
867 
868         /// <summary>
869         /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
870         /// Uses the specified project collection.
871         /// </summary>
Create(string path, ProjectCollection projectCollection, NewProjectFileOptions newProjectFileOptions)872         public static ProjectRootElement Create(string path, ProjectCollection projectCollection, NewProjectFileOptions newProjectFileOptions)
873         {
874             ErrorUtilities.VerifyThrowArgumentLength(path, "path");
875             ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection");
876 
877             ProjectRootElement projectRootElement = new ProjectRootElement(projectCollection.ProjectRootElementCache, newProjectFileOptions);
878             projectRootElement.FullPath = path;
879 
880             return projectRootElement;
881         }
882 
883         /// <summary>
884         /// Initialize a ProjectRootElement instance from an XmlReader.
885         /// Uses the global project collection.
886         /// May throw InvalidProjectFileException.
887         /// </summary>
Create(XmlReader xmlReader)888         public static ProjectRootElement Create(XmlReader xmlReader)
889         {
890             return Create(xmlReader, ProjectCollection.GlobalProjectCollection, preserveFormatting: false);
891         }
892 
893         /// <summary>
894         /// Initialize a ProjectRootElement instance from an XmlReader.
895         /// Uses the specified project collection.
896         /// May throw InvalidProjectFileException.
897         /// </summary>
Create(XmlReader xmlReader, ProjectCollection projectCollection)898         public static ProjectRootElement Create(XmlReader xmlReader, ProjectCollection projectCollection)
899         {
900             return Create(xmlReader, projectCollection, preserveFormatting: false);
901         }
902 
903         /// <summary>
904         /// Initialize a ProjectRootElement instance from an XmlReader.
905         /// Uses the specified project collection.
906         /// May throw InvalidProjectFileException.
907         /// </summary>
Create(XmlReader xmlReader, ProjectCollection projectCollection, bool preserveFormatting)908         public static ProjectRootElement Create(XmlReader xmlReader, ProjectCollection projectCollection, bool preserveFormatting)
909         {
910             ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection");
911 
912             return new ProjectRootElement(xmlReader, projectCollection.ProjectRootElementCache, true /*Explicitly loaded*/,
913                 preserveFormatting);
914         }
915 
916         /// <summary>
917         /// Initialize a ProjectRootElement instance by loading from the specified file path.
918         /// Uses the global project collection.
919         /// May throw InvalidProjectFileException.
920         /// </summary>
Open(string path)921         public static ProjectRootElement Open(string path)
922         {
923             return Open(path, ProjectCollection.GlobalProjectCollection);
924         }
925 
926         /// <summary>
927         /// Initialize a ProjectRootElement instance by loading from the specified file path.
928         /// Uses the specified project collection.
929         /// May throw InvalidProjectFileException.
930         /// </summary>
Open(string path, ProjectCollection projectCollection)931         public static ProjectRootElement Open(string path, ProjectCollection projectCollection)
932         {
933             return Open(path, projectCollection,
934                 preserveFormatting: null);
935         }
936 
937         /// <summary>
938         /// Initialize a ProjectRootElement instance by loading from the specified file path.
939         /// Uses the specified project collection and preserves the formatting of the document if specified.
940         /// </summary>
Open(string path, ProjectCollection projectCollection, bool? preserveFormatting)941         public static ProjectRootElement Open(string path, ProjectCollection projectCollection, bool? preserveFormatting)
942         {
943             ErrorUtilities.VerifyThrowArgumentLength(path, "path");
944             ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection");
945 
946             path = FileUtilities.NormalizePath(path);
947 
948             return Open(path, projectCollection.ProjectRootElementCache, true /*Is explicitly loaded*/, preserveFormatting);
949         }
950 
951         /// <summary>
952         /// Returns the ProjectRootElement for the given path if it has been loaded, or null if it is not currently in memory.
953         /// Uses the global project collection.
954         /// </summary>
955         /// <param name="path">The path of the ProjectRootElement, cannot be null.</param>
956         /// <returns>The loaded ProjectRootElement, or null if it is not currently in memory.</returns>
957         /// <remarks>
958         /// It is possible for ProjectRootElements to be brought into memory and discarded due to memory pressure. Therefore
959         /// this method returning false does not indicate that it has never been loaded, only that it is not currently in memory.
960         /// </remarks>
TryOpen(string path)961         public static ProjectRootElement TryOpen(string path)
962         {
963             ErrorUtilities.VerifyThrowArgumentLength(path, "path");
964 
965             return TryOpen(path, ProjectCollection.GlobalProjectCollection);
966         }
967 
968         /// <summary>
969         /// Returns the ProjectRootElement for the given path if it has been loaded, or null if it is not currently in memory.
970         /// Uses the specified project collection.
971         /// </summary>
972         /// <param name="path">The path of the ProjectRootElement, cannot be null.</param>
973         /// <param name="projectCollection">The <see cref="ProjectCollection"/> to load the project into.</param>
974         /// <returns>The loaded ProjectRootElement, or null if it is not currently in memory.</returns>
975         /// <remarks>
976         /// It is possible for ProjectRootElements to be brought into memory and discarded due to memory pressure. Therefore
977         /// this method returning false does not indicate that it has never been loaded, only that it is not currently in memory.
978         /// </remarks>
TryOpen(string path, ProjectCollection projectCollection)979         public static ProjectRootElement TryOpen(string path, ProjectCollection projectCollection)
980         {
981             return TryOpen(path, projectCollection, preserveFormatting: null);
982         }
983 
984         /// <summary>
985         /// Returns the ProjectRootElement for the given path if it has been loaded, or null if it is not currently in memory.
986         /// Uses the specified project collection.
987         /// </summary>
988         /// <param name="path">The path of the ProjectRootElement, cannot be null.</param>
989         /// <param name="projectCollection">The <see cref="ProjectCollection"/> to load the project into.</param>
990         /// <param name="preserveFormatting">
991         /// The formatting to open with. Must match the formatting in the collection to succeed.
992         /// </param>
993         /// <returns>The loaded ProjectRootElement, or null if it is not currently in memory.</returns>
994         /// <remarks>
995         /// It is possible for ProjectRootElements to be brought into memory and discarded due to memory pressure. Therefore
996         /// this method returning false does not indicate that it has never been loaded, only that it is not currently in memory.
997         /// </remarks>
TryOpen(string path, ProjectCollection projectCollection, bool? preserveFormatting)998         public static ProjectRootElement TryOpen(string path, ProjectCollection projectCollection, bool? preserveFormatting)
999         {
1000             ErrorUtilities.VerifyThrowArgumentLength(path, "path");
1001             ErrorUtilities.VerifyThrowArgumentNull(projectCollection, "projectCollection");
1002 
1003             path = FileUtilities.NormalizePath(path);
1004 
1005             ProjectRootElement projectRootElement = projectCollection.ProjectRootElementCache.TryGet(path, preserveFormatting);
1006 
1007             return projectRootElement;
1008         }
1009 
1010         /// <summary>
1011         /// Convenience method that picks a location based on a heuristic:
1012         /// If import groups exist, inserts into the last one without a condition on it.
1013         /// Otherwise, creates an import at the end of the project.
1014         /// </summary>
AddImport(string project)1015         public ProjectImportElement AddImport(string project)
1016         {
1017             ErrorUtilities.VerifyThrowArgumentLength(project, "project");
1018 
1019             ProjectImportGroupElement importGroupToAddTo =
1020                 ImportGroupsReversed.FirstOrDefault(importGroup => importGroup.Condition.Length <= 0);
1021 
1022             ProjectImportElement import;
1023 
1024             if (importGroupToAddTo != null)
1025             {
1026                 import = importGroupToAddTo.AddImport(project);
1027             }
1028             else
1029             {
1030                 import = CreateImportElement(project);
1031                 AppendChild(import);
1032             }
1033 
1034             return import;
1035         }
1036 
1037         /// <summary>
1038         /// Convenience method that picks a location based on a heuristic:
1039         /// Creates an import group at the end of the project.
1040         /// </summary>
AddImportGroup()1041         public ProjectImportGroupElement AddImportGroup()
1042         {
1043             ProjectImportGroupElement importGroup = CreateImportGroupElement();
1044             AppendChild(importGroup);
1045 
1046             return importGroup;
1047         }
1048 
1049         /// <summary>
1050         /// Convenience method that picks a location based on a heuristic:
1051         /// Finds item group with no condition with at least one item of same type, or else adds a new item group;
1052         /// adds the item to that item group with items of the same type, ordered by include.
1053         /// </summary>
1054         /// <remarks>
1055         /// Per the previous implementation, it actually finds the last suitable item group, not the first.
1056         /// </remarks>
AddItem(string itemType, string include)1057         public ProjectItemElement AddItem(string itemType, string include)
1058         {
1059             return AddItem(itemType, include, null);
1060         }
1061 
1062         /// <summary>
1063         /// Convenience method that picks a location based on a heuristic:
1064         /// Finds first item group with no condition with at least one item of same type, or else an empty item group; or else adds a new item group;
1065         /// adds the item to that item group with items of the same type, ordered by include.
1066         /// Does not attempt to check whether the item matches an existing wildcard expression; that is only possible
1067         /// in the evaluated world.
1068         /// </summary>
1069         /// <remarks>
1070         /// Per the previous implementation, it actually finds the last suitable item group, not the first.
1071         /// </remarks>
AddItem(string itemType, string include, IEnumerable<KeyValuePair<string, string>> metadata)1072         public ProjectItemElement AddItem(string itemType, string include, IEnumerable<KeyValuePair<string, string>> metadata)
1073         {
1074             ErrorUtilities.VerifyThrowArgumentLength(itemType, "itemType");
1075             ErrorUtilities.VerifyThrowArgumentLength(include, "include");
1076 
1077             ProjectItemGroupElement itemGroupToAddTo = null;
1078 
1079             foreach (ProjectItemGroupElement itemGroup in ItemGroups)
1080             {
1081                 if (itemGroup.Condition.Length > 0)
1082                 {
1083                     continue;
1084                 }
1085 
1086                 if (itemGroupToAddTo == null && itemGroup.Count == 0)
1087                 {
1088                     itemGroupToAddTo = itemGroup;
1089                 }
1090 
1091                 if (itemGroup.Items.Any(item => MSBuildNameIgnoreCaseComparer.Default.Equals(itemType, item.ItemType)))
1092                 {
1093                     itemGroupToAddTo = itemGroup;
1094                 }
1095 
1096                 if (itemGroupToAddTo != null && itemGroupToAddTo.Count > 0)
1097                 {
1098                     break;
1099                 }
1100             }
1101 
1102             if (itemGroupToAddTo == null)
1103             {
1104                 itemGroupToAddTo = AddItemGroup();
1105             }
1106 
1107             ProjectItemElement newItem = itemGroupToAddTo.AddItem(itemType, include, metadata);
1108 
1109             return newItem;
1110         }
1111 
1112         /// <summary>
1113         /// Convenience method that picks a location based on a heuristic:
1114         /// Adds an item group after the last existing item group, if any; otherwise
1115         /// adds an item group after the last existing property group, if any; otherwise
1116         /// adds a new item group at the end of the project.
1117         /// </summary>
AddItemGroup()1118         public ProjectItemGroupElement AddItemGroup()
1119         {
1120             ProjectElement reference = ItemGroupsReversed.FirstOrDefault();
1121 
1122             if (reference == null)
1123             {
1124                 foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroupsReversed)
1125                 {
1126                     reference = propertyGroup;
1127                     break;
1128                 }
1129             }
1130 
1131             ProjectItemGroupElement newItemGroup = CreateItemGroupElement();
1132 
1133             if (reference == null)
1134             {
1135                 AppendChild(newItemGroup);
1136             }
1137             else
1138             {
1139                 InsertAfterChild(newItemGroup, reference);
1140             }
1141 
1142             return newItemGroup;
1143         }
1144 
1145         /// <summary>
1146         /// Convenience method that picks a location based on a heuristic:
1147         /// Finds first item definition group with no condition with at least one item definition of same item type, or else adds a new item definition group.
1148         /// </summary>
AddItemDefinition(string itemType)1149         public ProjectItemDefinitionElement AddItemDefinition(string itemType)
1150         {
1151             ErrorUtilities.VerifyThrowArgumentLength(itemType, "itemType");
1152 
1153             ProjectItemDefinitionGroupElement itemDefinitionGroupToAddTo = null;
1154 
1155             foreach (ProjectItemDefinitionGroupElement itemDefinitionGroup in ItemDefinitionGroups)
1156             {
1157                 if (itemDefinitionGroup.Condition.Length > 0)
1158                 {
1159                     continue;
1160                 }
1161 
1162                 foreach (ProjectItemDefinitionElement itemDefinition in itemDefinitionGroup.ItemDefinitions)
1163                 {
1164                     if (MSBuildNameIgnoreCaseComparer.Default.Equals(itemType, itemDefinition.ItemType))
1165                     {
1166                         itemDefinitionGroupToAddTo = itemDefinitionGroup;
1167                         break;
1168                     }
1169                 }
1170 
1171                 if (itemDefinitionGroupToAddTo != null)
1172                 {
1173                     break;
1174                 }
1175             }
1176 
1177             if (itemDefinitionGroupToAddTo == null)
1178             {
1179                 itemDefinitionGroupToAddTo = AddItemDefinitionGroup();
1180             }
1181 
1182             ProjectItemDefinitionElement newItemDefinition = CreateItemDefinitionElement(itemType);
1183 
1184             itemDefinitionGroupToAddTo.AppendChild(newItemDefinition);
1185 
1186             return newItemDefinition;
1187         }
1188 
1189         /// <summary>
1190         /// Convenience method that picks a location based on a heuristic:
1191         /// Adds an item definition group after the last existing item definition group, if any; otherwise
1192         /// adds an item definition group after the last existing property group, if any; otherwise
1193         /// adds a new item definition group at the end of the project.
1194         /// </summary>
AddItemDefinitionGroup()1195         public ProjectItemDefinitionGroupElement AddItemDefinitionGroup()
1196         {
1197             ProjectElement reference = null;
1198 
1199             foreach (ProjectItemDefinitionGroupElement itemDefinitionGroup in ItemDefinitionGroupsReversed)
1200             {
1201                 reference = itemDefinitionGroup;
1202                 break;
1203             }
1204 
1205             if (reference == null)
1206             {
1207                 foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroupsReversed)
1208                 {
1209                     reference = propertyGroup;
1210                     break;
1211                 }
1212             }
1213 
1214             ProjectItemDefinitionGroupElement newItemDefinitionGroup = CreateItemDefinitionGroupElement();
1215 
1216             InsertAfterChild(newItemDefinitionGroup, reference);
1217 
1218             return newItemDefinitionGroup;
1219         }
1220 
1221         /// <summary>
1222         /// Convenience method that picks a location based on a heuristic:
1223         /// Adds a new property group after the last existing property group, if any; otherwise
1224         /// at the start of the project.
1225         /// </summary>
AddPropertyGroup()1226         public ProjectPropertyGroupElement AddPropertyGroup()
1227         {
1228             ProjectPropertyGroupElement reference = null;
1229 
1230             foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroupsReversed)
1231             {
1232                 reference = propertyGroup;
1233                 break;
1234             }
1235 
1236             ProjectPropertyGroupElement newPropertyGroup = CreatePropertyGroupElement();
1237 
1238             InsertAfterChild(newPropertyGroup, reference);
1239 
1240             return newPropertyGroup;
1241         }
1242 
1243         /// <summary>
1244         /// Convenience method that picks a location based on a heuristic.
1245         /// Updates the last existing property with the specified name that has no condition on itself or its property group, if any.
1246         /// Otherwise, adds a new property in the first property group without a condition, creating a property group if necessary after
1247         /// the last existing property group, else at the start of the project.
1248         /// </summary>
AddProperty(string name, string value)1249         public ProjectPropertyElement AddProperty(string name, string value)
1250         {
1251             ProjectPropertyGroupElement matchingPropertyGroup = null;
1252             ProjectPropertyElement matchingProperty = null;
1253 
1254             foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroups)
1255             {
1256                 if (propertyGroup.Condition.Length > 0)
1257                 {
1258                     continue;
1259                 }
1260 
1261                 if (matchingPropertyGroup == null)
1262                 {
1263                     matchingPropertyGroup = propertyGroup;
1264                 }
1265 
1266                 foreach (ProjectPropertyElement property in propertyGroup.Properties)
1267                 {
1268                     if (property.Condition.Length > 0)
1269                     {
1270                         continue;
1271                     }
1272 
1273                     if (MSBuildNameIgnoreCaseComparer.Default.Equals(property.Name, name))
1274                     {
1275                         matchingProperty = property;
1276                     }
1277                 }
1278             }
1279 
1280             if (matchingProperty != null)
1281             {
1282                 matchingProperty.Value = value;
1283 
1284                 return matchingProperty;
1285             }
1286 
1287             if (matchingPropertyGroup == null)
1288             {
1289                 matchingPropertyGroup = AddPropertyGroup();
1290             }
1291 
1292             ProjectPropertyElement newProperty = matchingPropertyGroup.AddProperty(name, value);
1293 
1294             return newProperty;
1295         }
1296 
1297         /// <summary>
1298         /// Convenience method that picks a location based on a heuristic:
1299         /// Creates a target at the end of the project.
1300         /// </summary>
AddTarget(string name)1301         public ProjectTargetElement AddTarget(string name)
1302         {
1303             ProjectTargetElement target = CreateTargetElement(name);
1304             AppendChild(target);
1305 
1306             return target;
1307         }
1308 
1309         /// <summary>
1310         /// Convenience method that picks a location based on a heuristic:
1311         /// Creates a usingtask at the end of the project.
1312         /// Exactly one of assemblyName or assemblyFile must be null.
1313         /// </summary>
AddUsingTask(string name, string assemblyFile, string assemblyName)1314         public ProjectUsingTaskElement AddUsingTask(string name, string assemblyFile, string assemblyName)
1315         {
1316             ProjectUsingTaskElement usingTask = CreateUsingTaskElement(name, FileUtilities.FixFilePath(assemblyFile), assemblyName);
1317             AppendChild(usingTask);
1318 
1319             return usingTask;
1320         }
1321 
1322         /// <summary>
1323         /// Creates a choose.
1324         /// Caller must add it to the location of choice in the project.
1325         /// </summary>
CreateChooseElement()1326         public ProjectChooseElement CreateChooseElement()
1327         {
1328             return ProjectChooseElement.CreateDisconnected(this);
1329         }
1330 
1331         /// <summary>
1332         /// Creates an import.
1333         /// Caller must add it to the location of choice in the project.
1334         /// </summary>
CreateImportElement(string project)1335         public ProjectImportElement CreateImportElement(string project)
1336         {
1337             return ProjectImportElement.CreateDisconnected(project, this);
1338         }
1339 
1340         /// <summary>
1341         /// Creates an item node.
1342         /// Caller must add it to the location of choice in the project.
1343         /// </summary>
CreateItemElement(string itemType)1344         public ProjectItemElement CreateItemElement(string itemType)
1345         {
1346             return ProjectItemElement.CreateDisconnected(itemType, this);
1347         }
1348 
1349         /// <summary>
1350         /// Creates an item node with an include.
1351         /// Caller must add it to the location of choice in the project.
1352         /// </summary>
CreateItemElement(string itemType, string include)1353         public ProjectItemElement CreateItemElement(string itemType, string include)
1354         {
1355             ProjectItemElement item = ProjectItemElement.CreateDisconnected(itemType, this);
1356 
1357             item.Include = include;
1358 
1359             return item;
1360         }
1361 
1362         /// <summary>
1363         /// Creates an item definition.
1364         /// Caller must add it to the location of choice in the project.
1365         /// </summary>
CreateItemDefinitionElement(string itemType)1366         public ProjectItemDefinitionElement CreateItemDefinitionElement(string itemType)
1367         {
1368             return ProjectItemDefinitionElement.CreateDisconnected(itemType, this);
1369         }
1370 
1371         /// <summary>
1372         /// Creates an item definition group.
1373         /// Caller must add it to the location of choice in the project.
1374         /// </summary>
CreateItemDefinitionGroupElement()1375         public ProjectItemDefinitionGroupElement CreateItemDefinitionGroupElement()
1376         {
1377             return ProjectItemDefinitionGroupElement.CreateDisconnected(this);
1378         }
1379 
1380         /// <summary>
1381         /// Creates an item group.
1382         /// Caller must add it to the location of choice in the project.
1383         /// </summary>
CreateItemGroupElement()1384         public ProjectItemGroupElement CreateItemGroupElement()
1385         {
1386             return ProjectItemGroupElement.CreateDisconnected(this);
1387         }
1388 
1389         /// <summary>
1390         /// Creates an import group.
1391         /// Caller must add it to the location of choice in the project.
1392         /// </summary>
CreateImportGroupElement()1393         public ProjectImportGroupElement CreateImportGroupElement()
1394         {
1395             return ProjectImportGroupElement.CreateDisconnected(this);
1396         }
1397 
1398         /// <summary>
1399         /// Creates a metadata node.
1400         /// Caller must add it to the location of choice in the project.
1401         /// </summary>
CreateMetadataElement(string name)1402         public ProjectMetadataElement CreateMetadataElement(string name)
1403         {
1404             return ProjectMetadataElement.CreateDisconnected(name, this);
1405         }
1406 
1407         /// <summary>
1408         /// Creates a metadata node.
1409         /// Caller must add it to the location of choice in the project.
1410         /// </summary>
CreateMetadataElement(string name, string unevaluatedValue)1411         public ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue)
1412         {
1413             ProjectMetadataElement metadatum = ProjectMetadataElement.CreateDisconnected(name, this);
1414 
1415             metadatum.Value = unevaluatedValue;
1416 
1417             return metadatum;
1418         }
1419 
1420         /// <summary>
1421         /// Creates an on error node.
1422         /// Caller must add it to the location of choice in the project.
1423         /// </summary>
CreateOnErrorElement(string executeTargets)1424         public ProjectOnErrorElement CreateOnErrorElement(string executeTargets)
1425         {
1426             return ProjectOnErrorElement.CreateDisconnected(executeTargets, this);
1427         }
1428 
1429         /// <summary>
1430         /// Creates an otherwise node.
1431         /// Caller must add it to the location of choice in the project.
1432         /// </summary>
CreateOtherwiseElement()1433         public ProjectOtherwiseElement CreateOtherwiseElement()
1434         {
1435             return ProjectOtherwiseElement.CreateDisconnected(this);
1436         }
1437 
1438         /// <summary>
1439         /// Creates an output node.
1440         /// Exactly one of itemType and propertyName must be specified.
1441         /// Caller must add it to the location of choice in the project.
1442         /// </summary>
CreateOutputElement(string taskParameter, string itemType, string propertyName)1443         public ProjectOutputElement CreateOutputElement(string taskParameter, string itemType, string propertyName)
1444         {
1445             return ProjectOutputElement.CreateDisconnected(taskParameter, itemType, propertyName, this);
1446         }
1447 
1448         /// <summary>
1449         /// Creates a project extensions node.
1450         /// Caller must add it to the location of choice in the project.
1451         /// </summary>
CreateProjectExtensionsElement()1452         public ProjectExtensionsElement CreateProjectExtensionsElement()
1453         {
1454             return ProjectExtensionsElement.CreateDisconnected(this);
1455         }
1456 
1457         /// <summary>
1458         /// Creates a property group.
1459         /// Caller must add it to the location of choice in the project.
1460         /// </summary>
CreatePropertyGroupElement()1461         public ProjectPropertyGroupElement CreatePropertyGroupElement()
1462         {
1463             return ProjectPropertyGroupElement.CreateDisconnected(this);
1464         }
1465 
1466         /// <summary>
1467         /// Creates a property.
1468         /// Caller must add it to the location of choice in the project.
1469         /// </summary>
CreatePropertyElement(string name)1470         public ProjectPropertyElement CreatePropertyElement(string name)
1471         {
1472             return ProjectPropertyElement.CreateDisconnected(name, this);
1473         }
1474 
1475         /// <summary>
1476         /// Creates a target.
1477         /// Caller must add it to the location of choice in this project.
1478         /// </summary>
CreateTargetElement(string name)1479         public ProjectTargetElement CreateTargetElement(string name)
1480         {
1481             return ProjectTargetElement.CreateDisconnected(name, this);
1482         }
1483 
1484         /// <summary>
1485         /// Creates a task.
1486         /// Caller must add it to the location of choice in this project.
1487         /// </summary>
CreateTaskElement(string name)1488         public ProjectTaskElement CreateTaskElement(string name)
1489         {
1490             return ProjectTaskElement.CreateDisconnected(name, this);
1491         }
1492 
1493         /// <summary>
1494         /// Creates a using task.
1495         /// Caller must add it to the location of choice in the project.
1496         /// Exactly one of assembly file and assembly name must be provided.
1497         /// </summary>
CreateUsingTaskElement(string taskName, string assemblyFile, string assemblyName)1498         public ProjectUsingTaskElement CreateUsingTaskElement(string taskName, string assemblyFile, string assemblyName)
1499         {
1500             return CreateUsingTaskElement(taskName, assemblyFile, assemblyName, null, null);
1501         }
1502 
1503         /// <summary>
1504         /// Creates a using task.
1505         /// Caller must add it to the location of choice in the project.
1506         /// Exactly one of assembly file and assembly name must be provided.
1507         /// Also allows providing optional runtime and architecture specifiers.  Null is OK.
1508         /// </summary>
CreateUsingTaskElement(string taskName, string assemblyFile, string assemblyName, string runtime, string architecture)1509         public ProjectUsingTaskElement CreateUsingTaskElement(string taskName, string assemblyFile, string assemblyName, string runtime, string architecture)
1510         {
1511             return ProjectUsingTaskElement.CreateDisconnected(taskName, assemblyFile, assemblyName, runtime, architecture, this);
1512         }
1513 
1514         /// <summary>
1515         /// Creates a ParameterGroup for use in a using task.
1516         /// Caller must add it to the location of choice in the project under a using task.
1517         /// </summary>
CreateUsingTaskParameterGroupElement()1518         public UsingTaskParameterGroupElement CreateUsingTaskParameterGroupElement()
1519         {
1520             return UsingTaskParameterGroupElement.CreateDisconnected(this);
1521         }
1522 
1523         /// <summary>
1524         /// Creates a Parameter for use in a using ParameterGroup.
1525         /// Caller must add it to the location of choice in the project under a using task.
1526         /// </summary>
CreateUsingTaskParameterElement(string name, string output, string required, string parameterType)1527         public ProjectUsingTaskParameterElement CreateUsingTaskParameterElement(string name, string output, string required, string parameterType)
1528         {
1529             return ProjectUsingTaskParameterElement.CreateDisconnected(name, output, required, parameterType, this);
1530         }
1531 
1532         /// <summary>
1533         /// Creates a Task element for use in a using task.
1534         /// Caller must add it to the location of choice in the project under a using task.
1535         /// </summary>
CreateUsingTaskBodyElement(string evaluate, string body)1536         public ProjectUsingTaskBodyElement CreateUsingTaskBodyElement(string evaluate, string body)
1537         {
1538             return ProjectUsingTaskBodyElement.CreateDisconnected(evaluate, body, this);
1539         }
1540 
1541         /// <summary>
1542         /// Creates a when.
1543         /// Caller must add it to the location of choice in this project.
1544         /// </summary>
CreateWhenElement(string condition)1545         public ProjectWhenElement CreateWhenElement(string condition)
1546         {
1547             return ProjectWhenElement.CreateDisconnected(condition, this);
1548         }
1549 
1550         /// <summary>
1551         /// Creates a project SDK element attached to this project.
1552         /// </summary>
CreateProjectSdkElement(string sdkName, string sdkVersion)1553         public ProjectSdkElement CreateProjectSdkElement(string sdkName, string sdkVersion)
1554         {
1555             return ProjectSdkElement.CreateDisconnected(sdkName, sdkVersion, this);
1556         }
1557 
1558         /// <summary>
1559         /// Save the project to the file system, if dirty.
1560         /// Uses the Encoding returned by the Encoding property.
1561         /// Creates any necessary directories.
1562         /// May throw IO-related exceptions.
1563         /// Clears the dirty flag.
1564         /// </summary>
Save()1565         public void Save()
1566         {
1567             Save(Encoding);
1568         }
1569 
1570         /// <summary>
1571         /// Save the project to the file system, if dirty.
1572         /// Creates any necessary directories.
1573         /// May throw IO-related exceptions.
1574         /// Clears the dirty flag.
1575         /// </summary>
Save(Encoding saveEncoding)1576         public void Save(Encoding saveEncoding)
1577         {
1578             ErrorUtilities.VerifyThrowInvalidOperation(_projectFileLocation != null, "OM_MustSetFileNameBeforeSave");
1579 
1580 #if MSBUILDENABLEVSPROFILING
1581             try
1582             {
1583                 string beginProjectSave = String.Format(CultureInfo.CurrentCulture, "Save Project {0} To File - Begin", projectFileLocation.File);
1584                 DataCollection.CommentMarkProfile(8810, beginProjectSave);
1585 #endif
1586 
1587             Directory.CreateDirectory(DirectoryPath);
1588 #if (!STANDALONEBUILD)
1589             using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildProjectSaveToFileBegin, CodeMarkerEvent.perfMSBuildProjectSaveToFileEnd))
1590 #endif
1591             {
1592                 // Note: We're using string Equals on encoding and not EncodingUtilities.SimilarToEncoding in order
1593                 // to force a save if the Encoding changed from UTF8 with BOM to UTF8 w/o BOM (for example).
1594                 if (HasUnsavedChanges || !Equals(saveEncoding, Encoding))
1595                 {
1596                     using (ProjectWriter projectWriter = new ProjectWriter(_projectFileLocation.File, saveEncoding))
1597                     {
1598                         projectWriter.Initialize(XmlDocument);
1599                         XmlDocument.Save(projectWriter);
1600                     }
1601 
1602                     _encoding = saveEncoding;
1603 
1604                     FileInfo fileInfo = FileUtilities.GetFileInfoNoThrow(_projectFileLocation.File);
1605 
1606                     // If the file was deleted by a race with someone else immediately after it was written above
1607                     // then we obviously can't read the write time. In this obscure case, we'll retain the
1608                     // older last write time, which at worst would cause the next load to unnecessarily
1609                     // come from disk.
1610                     if (fileInfo != null)
1611                     {
1612                         _lastWriteTimeWhenRead = fileInfo.LastWriteTime;
1613                     }
1614 
1615                     _versionOnDisk = Version;
1616                 }
1617             }
1618 #if MSBUILDENABLEVSPROFILING
1619             }
1620             finally
1621             {
1622                 string endProjectSave = String.Format(CultureInfo.CurrentCulture, "Save Project {0} To File - End", projectFileLocation.File);
1623                 DataCollection.CommentMarkProfile(8811, endProjectSave);
1624             }
1625 #endif
1626         }
1627 
1628         /// <summary>
1629         /// Save the project to the file system, if dirty or the path is different.
1630         /// Creates any necessary directories.
1631         /// May throw IO related exceptions.
1632         /// Clears the Dirty flag.
1633         /// </summary>
Save(string path)1634         public void Save(string path)
1635         {
1636             Save(path, Encoding);
1637         }
1638 
1639         /// <summary>
1640         /// Save the project to the file system, if dirty or the path is different.
1641         /// Creates any necessary directories.
1642         /// May throw IO related exceptions.
1643         /// Clears the Dirty flag.
1644         /// </summary>
Save(string path, Encoding encoding)1645         public void Save(string path, Encoding encoding)
1646         {
1647             FullPath = path;
1648 
1649             Save(encoding);
1650         }
1651 
1652         /// <summary>
1653         /// Save the project to the provided TextWriter, whether or not it is dirty.
1654         /// Uses the encoding of the TextWriter.
1655         /// Clears the Dirty flag.
1656         /// </summary>
Save(TextWriter writer)1657         public void Save(TextWriter writer)
1658         {
1659             using (ProjectWriter projectWriter = new ProjectWriter(writer))
1660             {
1661                 projectWriter.Initialize(XmlDocument);
1662                 XmlDocument.Save(projectWriter);
1663             }
1664 
1665             _versionOnDisk = Version;
1666         }
1667 
1668         /// <summary>
1669         /// Returns a clone of this project.
1670         /// </summary>
1671         /// <returns>The cloned element.</returns>
DeepClone()1672         public ProjectRootElement DeepClone()
1673         {
1674             return (ProjectRootElement)this.DeepClone(this, null);
1675         }
1676 
1677         /// <summary>
1678         /// Reload the existing project root element from its file.
1679         /// An <see cref="InvalidOperationException"/> is thrown if the project root element is not associated with any file on disk.
1680         ///
1681         /// See <see cref="ProjectRootElement.ReloadFrom(XmlReader, bool, bool?)"/>
1682         /// </summary>
Reload(bool throwIfUnsavedChanges = true, bool? preserveFormatting = null)1683         public void Reload(bool throwIfUnsavedChanges = true, bool? preserveFormatting = null)
1684         {
1685             ErrorUtilities.VerifyThrowInvalidOperation(!string.IsNullOrEmpty(FullPath), "ValueNotSet", $"{nameof(ProjectRootElement)}.{nameof(FullPath)}");
1686 
1687             ReloadFrom(FullPath, throwIfUnsavedChanges, preserveFormatting);
1688         }
1689 
1690         /// <summary>
1691         /// Reload the existing project root element from the given path
1692         /// An <see cref="InvalidOperationException"/> is thrown if the path does not exist.
1693         ///
1694         /// See <see cref="ProjectRootElement.ReloadFrom(XmlReader, bool, bool?)"/>
1695         /// </summary>
ReloadFrom(string path, bool throwIfUnsavedChanges = true, bool? preserveFormatting = null)1696         public void ReloadFrom(string path, bool throwIfUnsavedChanges = true, bool? preserveFormatting = null)
1697         {
1698             ErrorUtilities.VerifyThrowInvalidOperation(File.Exists(path), "FileToReloadFromDoesNotExist", path);
1699 
1700             Func<bool, XmlDocumentWithLocation> documentProducer = shouldPreserveFormatting => LoadDocument(path, shouldPreserveFormatting);
1701             ReloadFrom(documentProducer, throwIfUnsavedChanges, preserveFormatting);
1702         }
1703 
1704         /// <summary>
1705         /// Reload the existing project root element from the given <paramref name="reader"/>
1706         /// A reload operation completely replaces the state of this <see cref="ProjectRootElement"/> object. This operation marks the
1707         /// object as dirty (see <see cref="ProjectRootElement.MarkDirty"/> for side effects).
1708         ///
1709         /// If the new state has invalid XML or MSBuild syntax, then this method throws an <see cref="InvalidProjectFileException"/>.
1710         /// When this happens, the state of this object does not change.
1711         ///
1712         /// Reloading from an XMLReader will retain the previous root element location (<see cref="FullPath"/>, <see cref="DirectoryPath"/>, <see cref="ProjectFileLocation"/>).
1713         ///
1714         /// </summary>
1715         /// <param name="reader">Reader to read from</param>
1716         /// <param name="throwIfUnsavedChanges">
1717         ///   If set to false, the reload operation will discard any unsaved changes.
1718         ///   Otherwise, an <see cref="InvalidOperationException"/> is thrown when unsaved changes are present.
1719         /// </param>
1720         /// <param name="preserveFormatting">
1721         ///   Whether the reload should preserve formatting or not. A null value causes the reload to reuse the existing <see cref="PreserveFormatting"/> value.
1722         /// </param>
ReloadFrom(XmlReader reader, bool throwIfUnsavedChanges = true, bool? preserveFormatting = null)1723         public void ReloadFrom(XmlReader reader, bool throwIfUnsavedChanges = true, bool? preserveFormatting = null)
1724         {
1725             Func<bool, XmlDocumentWithLocation> documentProducer = shouldPreserveFormatting =>
1726             {
1727                 var document =  LoadDocument(reader, shouldPreserveFormatting);
1728 
1729                 document.FullPath = this.FullPath;
1730 
1731                 return document;
1732             };
1733 
1734             ReloadFrom(documentProducer, throwIfUnsavedChanges, preserveFormatting);
1735         }
1736 
ReloadFrom(Func<bool, XmlDocumentWithLocation> documentProducer, bool throwIfUnsavedChanges, bool? preserveFormatting)1737         private void ReloadFrom(Func<bool, XmlDocumentWithLocation> documentProducer, bool throwIfUnsavedChanges, bool? preserveFormatting)
1738         {
1739             ThrowIfUnsavedChanges(throwIfUnsavedChanges);
1740 
1741             XmlDocumentWithLocation document = documentProducer(preserveFormatting ?? PreserveFormatting);
1742 
1743             // Reload should only mutate the state if there are no parse errors.
1744             ThrowIfDocumentHasParsingErrors(document);
1745 
1746             // Do not clear the string cache.
1747             // Based on the assumption that Projects are reloaded repeatedly from their file with small increments,
1748             // and thus most strings would get reused
1749             //this.XmlDocument.ClearAnyCachedStrings();
1750 
1751             this.RemoveAllChildren();
1752 
1753             ProjectParser.Parse(document, this);
1754 
1755             MarkDirty("Project reloaded", null);
1756         }
1757 
1758         [MethodImpl(MethodImplOptions.NoOptimization)]
ThrowIfDocumentHasParsingErrors(XmlDocumentWithLocation document)1759         private static void ThrowIfDocumentHasParsingErrors(XmlDocumentWithLocation document)
1760         {
1761             // todo: rather than throw away, copy over the parse results
1762             var throwaway = new ProjectRootElement(document);
1763         }
1764 
1765         /// <summary>
1766         /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
1767         /// Uses the specified project root element cache.
1768         /// </summary>
Create(ProjectRootElementCache projectRootElementCache)1769         internal static ProjectRootElement Create(ProjectRootElementCache projectRootElementCache)
1770         {
1771             return new ProjectRootElement(projectRootElementCache, Project.DefaultNewProjectTemplateOptions);
1772         }
1773 
Create(ProjectRootElementCache projectRootElementCache, NewProjectFileOptions projectFileOptions)1774         internal static ProjectRootElement Create(ProjectRootElementCache projectRootElementCache, NewProjectFileOptions projectFileOptions)
1775         {
1776             return new ProjectRootElement(projectRootElementCache, projectFileOptions);
1777         }
1778 
1779         /// <summary>
1780         /// Initialize a ProjectRootElement instance by loading from the specified file path.
1781         /// Assumes path is already normalized.
1782         /// Uses the specified project root element cache.
1783         /// May throw InvalidProjectFileException.
1784         /// </summary>
Open(string path, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded, bool? preserveFormatting)1785         internal static ProjectRootElement Open(string path, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded,
1786             bool? preserveFormatting)
1787         {
1788             ErrorUtilities.VerifyThrowInternalRooted(path);
1789 
1790             ProjectRootElement projectRootElement = projectRootElementCache.Get(path,
1791                 preserveFormatting ?? false ? s_openLoaderPreserveFormattingDelegate : s_openLoaderDelegate,
1792                 isExplicitlyLoaded, preserveFormatting);
1793 
1794             return projectRootElement;
1795         }
1796 
1797         /// <summary>
1798         /// Initialize a ProjectRootElement instance from an existing document.
1799         /// Uses the global project collection.
1800         /// May throw InvalidProjectFileException.
1801         /// </summary>
1802         /// <remarks>
1803         /// This is ultimately for unit testing.
1804         /// Do not make public: we do not wish to expose particular XML API's.
1805         /// </remarks>
Open(XmlDocumentWithLocation document)1806         internal static ProjectRootElement Open(XmlDocumentWithLocation document)
1807         {
1808             ErrorUtilities.VerifyThrow(document.FullPath == null, "Only virtual documents supported");
1809 
1810             return new ProjectRootElement(document, ProjectCollection.GlobalProjectCollection.ProjectRootElementCache);
1811         }
1812 
1813         /// <summary>
1814         /// Gets a ProjectRootElement representing an MSBuild file.
1815         /// Path provided must be a canonicalized full path.
1816         /// May throw InvalidProjectFileException or an IO-related exception.
1817         /// </summary>
OpenProjectOrSolution(string fullPath, IDictionary<string, string> globalProperties, string toolsVersion, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded)1818         internal static ProjectRootElement OpenProjectOrSolution(string fullPath, IDictionary<string, string> globalProperties, string toolsVersion, ProjectRootElementCache projectRootElementCache, bool isExplicitlyLoaded)
1819         {
1820             ErrorUtilities.VerifyThrowInternalRooted(fullPath);
1821 
1822             ProjectRootElement projectRootElement = projectRootElementCache.Get(
1823                 fullPath,
1824                 (path, cache) => CreateProjectFromPath(path, globalProperties, toolsVersion, cache,
1825                                     preserveFormatting: false),
1826                 isExplicitlyLoaded,
1827                 // don't care about formatting, reuse whatever is there
1828                 preserveFormatting: null);
1829 
1830             return projectRootElement;
1831         }
1832 
1833         /// <summary>
1834         /// Creates a XmlElement with the specified name in the document
1835         /// containing this project.
1836         /// </summary>
CreateElement(string name)1837         internal XmlElementWithLocation CreateElement(string name)
1838         {
1839             return (XmlElementWithLocation)XmlDocument.CreateElement(name, XmlNamespace);
1840         }
1841 
1842         /// <summary>
1843         /// Overridden to verify that the potential parent and siblings
1844         /// are acceptable. Throws InvalidOperationException if they are not.
1845         /// </summary>
VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling)1846         internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling)
1847         {
1848             ErrorUtilities.ThrowInvalidOperation("OM_CannotAcceptParent");
1849         }
1850 
1851         /// <summary>
1852         /// Marks this project as dirty.
1853         /// Typically called by child elements to indicate that they themselves have become dirty.
1854         /// Accepts a reason for debugging purposes only, and optional reason parameter.
1855         /// </summary>
1856         /// <remarks>
1857         /// This is sealed because it is virtual and called in a constructor; by sealing it we
1858         /// satisfy FXCop that nobody will override it to do something that would rely on
1859         /// unconstructed state.
1860         /// Should be protected+internal.
1861         /// </remarks>
MarkDirty(string reason, string param)1862         internal sealed override void MarkDirty(string reason, string param)
1863         {
1864             IncrementVersion();
1865 
1866             _dirtyReason = reason;
1867             _dirtyParameter = param;
1868 
1869             _timeLastChangedUtc = DateTime.UtcNow;
1870 
1871             var changedEventArgs = new ProjectXmlChangedEventArgs(this, reason, param);
1872             var projectXmlChanged = OnProjectXmlChanged;
1873             projectXmlChanged?.Invoke(this, changedEventArgs);
1874 
1875             // Only bubble this event up if the cache knows about this PRE.
1876             if (this.IsMemberOfProjectCollection)
1877             {
1878                 _projectRootElementCache.OnProjectRootElementDirtied(this, changedEventArgs);
1879             }
1880         }
1881 
1882         /// <summary>
1883         /// Bubbles a Project dirty notification up to the ProjectRootElementCache and ultimately to the ProjectCollection.
1884         /// </summary>
1885         /// <param name="project">The dirtied project.</param>
MarkProjectDirty(Project project)1886         internal void MarkProjectDirty(Project project)
1887         {
1888             ErrorUtilities.VerifyThrowArgumentNull(project, "project");
1889 
1890             // Only bubble this event up if the cache knows about this PRE, which is equivalent to
1891             // whether this PRE has a path.
1892             if (_projectFileLocation != null)
1893             {
1894                 _projectRootElementCache.OnProjectDirtied(project, new ProjectChangedEventArgs(project));
1895             }
1896         }
1897 
1898         /// <summary>
1899         /// Sets the <see cref="IsExplicitlyLoaded"/> property to <c>true</c> to indicate that this PRE
1900         /// should not be removed from the cache until it is explicitly unloaded by some MSBuild client.
1901         /// </summary>
MarkAsExplicitlyLoaded()1902         internal void MarkAsExplicitlyLoaded()
1903         {
1904             IsExplicitlyLoaded = true;
1905         }
1906 
1907         /// <summary>
1908         /// Creates and returns a list of <see cref="ProjectImportElement"/> nodes which are implicitly
1909         /// referenced by the Project.
1910         /// </summary>
1911         /// <param name="currentProjectOrImport">Current project</param>
1912         /// <returns>An <see cref="IEnumerable{SdkReference}"/> containing details of the SDKs referenced by the project.</returns>
GetImplicitImportNodes(ProjectRootElement currentProjectOrImport)1913         internal List<ProjectImportElement> GetImplicitImportNodes(ProjectRootElement currentProjectOrImport)
1914         {
1915             var nodes = new List<ProjectImportElement>();
1916 
1917             string sdk = Sdk;
1918             if (!string.IsNullOrWhiteSpace(sdk))
1919             {
1920                 foreach (var referencedSdk in ParseSdks(sdk, SdkLocation))
1921                 {
1922                     nodes.Add(ProjectImportElement.CreateImplicit("Sdk.props", currentProjectOrImport, ImplicitImportLocation.Top, referencedSdk));
1923                     nodes.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, referencedSdk));
1924                 }
1925             }
1926 
1927             foreach (var sdkNode in Children.OfType<ProjectSdkElement>())
1928             {
1929                 var referencedSdk = new SdkReference(
1930                     sdkNode.XmlElement.GetAttribute("Name"),
1931                     sdkNode.XmlElement.GetAttribute("Version"),
1932                     sdkNode.XmlElement.GetAttribute("MinimumVersion"));
1933 
1934                 nodes.Add(ProjectImportElement.CreateImplicit("Sdk.props", currentProjectOrImport, ImplicitImportLocation.Top, referencedSdk));
1935                 nodes.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, referencedSdk));
1936             }
1937 
1938             return nodes;
1939         }
1940 
ParseSdks(string sdks, IElementLocation sdkLocation)1941         private static IEnumerable<SdkReference> ParseSdks(string sdks, IElementLocation sdkLocation)
1942         {
1943             foreach (string sdk in sdks.Split(';').Select(i => i.Trim()))
1944             {
1945                 SdkReference sdkReference;
1946 
1947                 if (!SdkReference.TryParse(sdk, out sdkReference))
1948                 {
1949                     ProjectErrorUtilities.ThrowInvalidProject(sdkLocation, "InvalidSdkFormat", sdks);
1950                 }
1951 
1952                 yield return sdkReference;
1953             }
1954         }
1955 
1956         /// <summary>
1957         /// Determines if the specified file is an empty XML file meaning it has no contents, contains only whitespace, or
1958         /// only an XML declaration.  If the file does not exist, it is not considered empty.
1959         /// </summary>
1960         /// <param name="path">The full path to a file to check.</param>
1961         /// <returns><code>true</code> if the file is an empty XML file, otherwise <code>false</code>.</returns>
IsEmptyXmlFile(string path)1962         internal static bool IsEmptyXmlFile(string path)
1963         {
1964             // The maximum number of characters of the file to read to check if its empty or not.  Ideally we
1965             // would only look at zero-length files but empty XML files can contain just an xml declaration:
1966             //
1967             //   <? xml version="1.0" encoding="utf-8" standalone="yes" ?>
1968             //
1969             // And this should also be treated as if the file is empty.
1970             //
1971             const int maxSizeToConsiderEmpty = 100;
1972 
1973             if (!File.Exists(path))
1974             {
1975                 // Non-existent files are not treated as empty
1976                 //
1977                 return false;
1978             }
1979 
1980             try
1981             {
1982                 FileInfo fileInfo = new FileInfo(path);
1983 
1984                 if (fileInfo.Length == 0)
1985                 {
1986                     // Zero length files are empty
1987                     //
1988                     return true;
1989                 }
1990 
1991                 if (fileInfo.Length > maxSizeToConsiderEmpty)
1992                 {
1993                     // Files greater than the maximum bytes to check are not empty
1994                     //
1995                     return false;
1996                 }
1997 
1998                 string contents = File.ReadAllText(path);
1999 
2000                 // If the file is only whitespace or the XML declaration then it empty
2001                 //
2002                 return String.IsNullOrEmpty(contents) || XmlDeclarationRegEx.Value.IsMatch(contents);
2003             }
2004             catch (Exception)
2005             {
2006                 // ignored
2007             }
2008 
2009             return false;
2010         }
2011 
2012         /// <summary>
2013         /// Returns a new instance of ProjectRootElement that is affiliated with the same ProjectRootElementCache.
2014         /// </summary>
2015         /// <param name="owner">The factory to use for creating the new instance.</param>
CreateNewInstance(ProjectRootElement owner)2016         protected override ProjectElement CreateNewInstance(ProjectRootElement owner)
2017         {
2018             return ProjectRootElement.Create(owner._projectRootElementCache);
2019         }
2020 
2021         /// <summary>
2022         /// Creates a new ProjectRootElement for a specific PRE cache
2023         /// </summary>
2024         /// <param name="path">The path to the file to load.</param>
2025         /// <param name="projectRootElementCache">The cache to load the PRE into.</param>
OpenLoader(string path, ProjectRootElementCache projectRootElementCache)2026         private static ProjectRootElement OpenLoader(string path, ProjectRootElementCache projectRootElementCache)
2027         {
2028             return OpenLoader(path, projectRootElementCache, preserveFormatting: false);
2029         }
2030 
OpenLoaderPreserveFormatting(string path, ProjectRootElementCache projectRootElementCache)2031         private static ProjectRootElement OpenLoaderPreserveFormatting(string path, ProjectRootElementCache projectRootElementCache)
2032         {
2033             return OpenLoader(path, projectRootElementCache, preserveFormatting: true);
2034         }
2035 
OpenLoader(string path, ProjectRootElementCache projectRootElementCache, bool preserveFormatting)2036         private static ProjectRootElement OpenLoader(string path, ProjectRootElementCache projectRootElementCache, bool preserveFormatting)
2037         {
2038             return new ProjectRootElement(
2039                 path,
2040                 projectRootElementCache,
2041                 preserveFormatting);
2042         }
2043 
2044         /// <summary>
2045         /// Creates a ProjectRootElement representing a file, where the file may be a .sln instead of
2046         /// an MSBuild format file.
2047         /// Assumes path is already normalized.
2048         /// If the file is in MSBuild format, may throw InvalidProjectFileException.
2049         /// If the file is a solution, will throw an IO-related exception if the file cannot be read.
2050         /// </summary>
CreateProjectFromPath( string projectFile, IDictionary<string, string> globalProperties, string toolsVersion, ProjectRootElementCache projectRootElementCache, bool preserveFormatting )2051         private static ProjectRootElement CreateProjectFromPath
2052             (
2053                 string projectFile,
2054                 IDictionary<string, string> globalProperties,
2055                 string toolsVersion,
2056                 ProjectRootElementCache projectRootElementCache,
2057                 bool preserveFormatting
2058             )
2059         {
2060             ErrorUtilities.VerifyThrowInternalRooted(projectFile);
2061 
2062             try
2063             {
2064                 if (FileUtilities.IsVCProjFilename(projectFile))
2065                 {
2066                     ProjectFileErrorUtilities.ThrowInvalidProjectFile(new BuildEventFileInfo(projectFile), "ProjectUpgradeNeededToVcxProj", projectFile);
2067                 }
2068 
2069                 // OK it's a regular project file, load it normally.
2070                 return new ProjectRootElement(projectFile, projectRootElementCache, preserveFormatting);
2071             }
2072             catch (InvalidProjectFileException)
2073             {
2074                 throw;
2075             }
2076             catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
2077             {
2078                 ProjectFileErrorUtilities.ThrowInvalidProjectFile(new BuildEventFileInfo(projectFile), ex, "InvalidProjectFile", ex.Message);
2079                 throw; // Without this there's a spurious CS0161 because csc 1.2.0.60317 can't see that the above is an unconditional throw.
2080             }
2081         }
2082 
2083         /// <summary>
2084         /// Constructor helper to load an XmlDocumentWithLocation from a path.
2085         /// Assumes path is already normalized.
2086         /// May throw InvalidProjectFileException.
2087         /// Never returns null.
2088         /// Does NOT add to the ProjectRootElementCache. Caller should add after verifying subsequent MSBuild parsing succeeds.
2089         /// </summary>
2090         /// <param name="fullPath">The full path to the document to load.</param>
2091         /// <param name="preserveFormatting"><code>true</code> to preserve the formatting of the document, otherwise <code>false</code>.</param>
LoadDocument(string fullPath, bool preserveFormatting)2092         private XmlDocumentWithLocation LoadDocument(string fullPath, bool preserveFormatting)
2093         {
2094             ErrorUtilities.VerifyThrowInternalRooted(fullPath);
2095 
2096             var document = new XmlDocumentWithLocation
2097             {
2098                 FullPath = fullPath,
2099                 PreserveWhitespace = preserveFormatting
2100             };
2101 #if (!STANDALONEBUILD)
2102             using (new CodeMarkerStartEnd(CodeMarkerEvent.perfMSBuildProjectLoadFromFileBegin, CodeMarkerEvent.perfMSBuildProjectLoadFromFileEnd))
2103 #endif
2104             {
2105                 try
2106                 {
2107 #if MSBUILDENABLEVSPROFILING
2108                     string beginProjectLoad = String.Format(CultureInfo.CurrentCulture, "Load Project {0} From File - Start", fullPath);
2109                     DataCollection.CommentMarkProfile(8806, beginProjectLoad);
2110 #endif
2111                     using (var xtr = XmlReaderExtension.Create(fullPath))
2112                     {
2113                         _encoding = xtr.Encoding;
2114                         document.Load(xtr.Reader);
2115                     }
2116 
2117                     _projectFileLocation = ElementLocation.Create(fullPath);
2118                     _directory = Path.GetDirectoryName(fullPath);
2119 
2120                     if (XmlDocument != null)
2121                     {
2122                         XmlDocument.FullPath = fullPath;
2123                     }
2124 
2125                     _lastWriteTimeWhenRead = FileUtilities.GetFileInfoNoThrow(fullPath).LastWriteTime;
2126                 }
2127                 catch (Exception ex)
2128                 {
2129                     if (ExceptionHandling.NotExpectedIoOrXmlException(ex))
2130                     {
2131                         throw;
2132                     }
2133 
2134                     XmlException xmlException = ex as XmlException;
2135 
2136                     BuildEventFileInfo fileInfo;
2137 
2138                     fileInfo = xmlException != null
2139                         ? new BuildEventFileInfo(fullPath, xmlException)
2140                         : new BuildEventFileInfo(fullPath);
2141 
2142                     ProjectFileErrorUtilities.ThrowInvalidProjectFile(fileInfo, ex, "InvalidProjectFile", ex.Message);
2143                 }
2144 #if MSBUILDENABLEVSPROFILING
2145                 finally
2146                 {
2147                     string endProjectLoad = String.Format(CultureInfo.CurrentCulture, "Load Project {0} From File - End", fullPath);
2148                     DataCollection.CommentMarkProfile(8807, endProjectLoad);
2149                 }
2150 #endif
2151             }
2152 
2153             return document;
2154         }
2155 
2156         /// <summary>
2157         /// Constructor helper to load an XmlDocumentWithLocation from an XmlReader.
2158         /// May throw InvalidProjectFileException.
2159         /// Never returns null.
2160         /// </summary>
LoadDocument(XmlReader reader, bool preserveFormatting)2161         private XmlDocumentWithLocation LoadDocument(XmlReader reader, bool preserveFormatting)
2162         {
2163             var document = new XmlDocumentWithLocation {PreserveWhitespace = preserveFormatting};
2164 
2165             try
2166             {
2167                 document.Load(reader);
2168             }
2169             catch (XmlException ex)
2170             {
2171                 BuildEventFileInfo fileInfo = new BuildEventFileInfo(ex);
2172 
2173                 ProjectFileErrorUtilities.ThrowInvalidProjectFile(fileInfo, "InvalidProjectFile", ex.Message);
2174             }
2175 
2176             return document;
2177         }
2178 
2179         /// <summary>
2180         /// Boost the appdomain-unique version counter for this object.
2181         /// This is done when it is modified, and also when it is loaded.
2182         /// </summary>
IncrementVersion()2183         private void IncrementVersion()
2184         {
2185             _version = Interlocked.Increment(ref s_globalVersionCounter);
2186         }
2187 
ThrowIfUnsavedChanges(bool throwIfUnsavedChanges)2188         private void ThrowIfUnsavedChanges(bool throwIfUnsavedChanges)
2189         {
2190             if (HasUnsavedChanges && throwIfUnsavedChanges)
2191             {
2192                 ErrorUtilities.ThrowInvalidOperation("NoReloadOnUnsavedChanges", null);
2193             }
2194         }
2195     }
2196 }
2197