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 using System;
5 using System.Xml;
6 using System.Collections;
7 using System.Collections.Generic;
8 using System.IO;
9 using System.Text;
10 using System.Globalization;
11 using System.Diagnostics;
12 
13 using Microsoft.Build.BuildEngine.Shared;
14 using Microsoft.Build.Framework;
15 
16 namespace Microsoft.Build.BuildEngine
17 {
18     /// <summary>
19     /// This class represents a collection of items.  It may be represented
20     /// physically by an &lt;ItemGroup&gt; element persisted in the project file,
21     /// or it may just be a virtual BuildItemGroup (e.g., the evaluated items).
22     /// </summary>
23     [DebuggerDisplay("BuildItemGroup (Count = { Count }, Condition = { Condition })")]
24     public class BuildItemGroup : IItemPropertyGrouping, IEnumerable
25     {
26         #region Member Data
27 
28         // Object holding the backing Xml, if any
29         private BuildItemGroupXml xml;
30 
31         // Whether there is backing Xml
32         // Can't use (xml==null) because it is consulted during construction of the backing Xml
33         // Can't use (parentProject!=null) because Clone() sets it to null, but expects to create a persisted group...
34         private bool isPersisted = false;
35 
36         // If this is a persisted <ItemGroup>, this boolean tells us whether
37         // it came from the main project file, or an imported project file.
38         bool importedFromAnotherProject = false;
39 
40         // These are the loose Items beneath this BuildItemGroup.  This is
41         // valid for both persisted and virtual ItemGroups.
42         private List<BuildItem> items;
43 
44         // If we are ever asked to back up our persisted items, we put the list
45         // in here, so we can restore them later.
46         private List<BuildItem> persistedItemBackup;
47 
48         // Collection propertygroup belongs to.
49         private GroupingCollection parentCollection = null;
50 
51         // If this is a persisted BuildItemGroup, it has a parent Project object.
52         private Project parentProject = null;
53 
54         #endregion
55 
56         #region Constructors
57 
58         /// <summary>
59         /// Default constructor, which initializes a virtual (non-persisted) BuildItemGroup.
60         /// </summary>
BuildItemGroup()61         public BuildItemGroup()
62         {
63             this.items = new List<BuildItem>();
64         }
65 
66         /// <summary>
67         /// This constructor initializes the BuildItemGroup from an &lt;ItemGroup&gt; element
68         /// in the project file.  It might come from the main project file or an
69         /// imported project file.
70         /// </summary>
BuildItemGroup(XmlElement itemGroupElement, bool importedFromAnotherProject, Project parentProject)71         internal BuildItemGroup(XmlElement itemGroupElement, bool importedFromAnotherProject, Project parentProject)
72             : this()
73         {
74             this.isPersisted = true;
75             this.xml = new BuildItemGroupXml(itemGroupElement);
76             this.importedFromAnotherProject = importedFromAnotherProject;
77             this.parentProject = parentProject;
78 
79             List<XmlElement> children = xml.GetChildren();
80             EnsureCapacity(children.Count);
81             for (int i = 0; i < children.Count; i++)
82             {
83                 AddExistingItem(new BuildItem(children[i], importedFromAnotherProject, parentProject.ItemDefinitionLibrary));
84             }
85         }
86 
87         /// <summary>
88         /// Constructor, which creates a new &lt;ItemGroup&gt; element in the XML document
89         /// specified.
90         /// </summary>
BuildItemGroup(XmlDocument ownerDocument, bool importedFromAnotherProject, Project parentProject)91         internal BuildItemGroup(XmlDocument ownerDocument, bool importedFromAnotherProject, Project parentProject)
92             : this()
93         {
94             this.isPersisted = true;
95             this.xml = new BuildItemGroupXml(ownerDocument);
96             this.importedFromAnotherProject = importedFromAnotherProject;
97             this.parentProject = parentProject;
98         }
99 
100         #endregion
101 
102         #region Properties
103 
104         /// <summary>
105         /// This returns a boolean telling you whether this particular item
106         /// group was imported from another project, or whether it was defined
107         /// in the main project.  For virtual item groups which have no
108         /// persistence, this is false.
109         /// </summary>
110         public bool IsImported
111         {
112             get { return importedFromAnotherProject; }
113         }
114 
115         /// <summary>
116         /// Accessor for the condition on the item group.
117         /// </summary>
118         public string Condition
119         {
120             get
121             {
122                 return (IsPersisted ? xml.Condition : String.Empty);
123             }
124 
125             set
126             {
127                 MustBePersisted("CannotSetCondition");
128                 MustNotBeImported();
129                 xml.Condition = value;
130                 MarkItemGroupAsDirty();
131             }
132         }
133 
134         /// <summary>
135         /// Accessor for the XmlElement representing this item.  This is internal
136         /// to MSBuild, and is read-only.
137         /// </summary>
138         internal XmlElement ItemGroupElement
139         {
140             get { return xml.Element; }
141         }
142 
143         /// <summary>
144         /// Accessor for the parent Project object.
145         /// </summary>
146         internal Project ParentProject
147         {
148             get { return this.parentProject; }
149         }
150 
151         /// <summary>
152         /// Setter for parent project field that makes explicit that's it's only for clearing it.
153         /// </summary>
ClearParentProject()154         internal void ClearParentProject()
155         {
156             parentProject = null;
157         }
158 
159         /// <summary>
160         /// Number of items in this group.
161         /// </summary>
162         public int Count
163         {
164             get { return items.Count; }
165         }
166 
167         /// <summary>
168         /// Gets the item at the specified index.
169         /// </summary>
170         public BuildItem this[int index]
171         {
172             get { return items[index]; }
173         }
174 
175         /// <summary>
176         /// Gets the actual list of items contained
177         /// in this group.
178         /// </summary>
179         internal List<BuildItem> Items
180         {
181             get { return items; }
182         }
183 
184         /// <summary>
185         /// Accessor for the ParentCollection that the BuildPropertyGroup belongs to
186         /// </summary>
187         internal GroupingCollection ParentCollection
188         {
189             get { return parentCollection; }
190             set { parentCollection = value; }
191         }
192 
193         /// <summary>
194         /// Accessor for the parent XML element
195         /// </summary>
196         internal XmlElement ParentElement
197         {
198             get { return xml.ParentElement; }
199         }
200         #endregion
201 
202         #region Methods
203 
204         /// <summary>
205         /// Copies the items in this group into a new array.
206         /// NOTE: the copies are NOT clones i.e. only the references are copied
207         /// </summary>
ToArray()208         public BuildItem[] ToArray()
209         {
210             return items.ToArray();
211         }
212 
213         /// <summary>
214         /// This IEnumerable method returns an IEnumerator object, which allows
215         /// the caller to enumerate through the BuildItem objects contained in
216         /// this BuildItemGroup.
217         /// </summary>
GetEnumerator()218         public IEnumerator GetEnumerator()
219         {
220             return items.GetEnumerator();
221         }
222 
223         /// <summary>
224         /// Import a bunch of items from another BuildItemGroup. This is an O(n) operation.
225         /// </summary>
ImportItems(BuildItemGroup itemsToImport)226         internal void ImportItems(BuildItemGroup itemsToImport)
227         {
228             ErrorUtilities.VerifyThrow(itemsToImport != null, "Null BuildItemGroup passed in.");
229             ErrorUtilities.VerifyThrow(itemsToImport != this, "Can't import into self.");
230 
231             EnsureCapacity(this.Count + itemsToImport.Count); // PERF: important to pre-size
232 
233             // Loop through all the Items in the given BuildItemGroup, and add them to
234             // our own BuildItemGroup.
235             foreach (BuildItem itemToImport in itemsToImport)
236             {
237                 AddItem(itemToImport);
238             }
239         }
240 
241         /// <summary>
242         /// Remove a bunch of items. This is an O(n) operation.
243         /// </summary>
RemoveItems(BuildItemGroup itemsToRemove)244         internal void RemoveItems(BuildItemGroup itemsToRemove)
245         {
246             ErrorUtilities.VerifyThrow(itemsToRemove != null, "Null BuildItemGroup passed in.");
247             ErrorUtilities.VerifyThrow(itemsToRemove != this, "Can't remove self.");
248 
249             foreach (BuildItem itemToRemove in itemsToRemove)
250             {
251                 RemoveItem(itemToRemove);
252             }
253         }
254 
RemoveItemsWithBackup(BuildItemGroup itemsToRemove)255         internal void RemoveItemsWithBackup(BuildItemGroup itemsToRemove)
256         {
257             ErrorUtilities.VerifyThrow(itemsToRemove != null, "Null BuildItemGroup passed in.");
258             ErrorUtilities.VerifyThrow(itemsToRemove != this, "Can't remove self.");
259 
260             foreach (BuildItem itemToRemove in itemsToRemove)
261             {
262                 RemoveItemWithBackup(itemToRemove);
263             }
264         }
265 
266         /// <summary>
267         /// Applies each of the item modifications in order.
268         /// Items are replaced with a virtual clone before they are modified.
269         /// If an item does not exist in this group, the modification is skipped.
270         /// If any modifications conflict, these modifications win.
271         /// Returns the cloned item made, or null if it does not exist in this group.
272         /// </summary>
ModifyItemAfterCloningUsingVirtualMetadata(BuildItem item, Dictionary<string, string> metadata)273         internal BuildItem ModifyItemAfterCloningUsingVirtualMetadata(BuildItem item, Dictionary<string, string> metadata)
274         {
275             int index = items.IndexOf(item);
276             if (index > -1)
277             {
278                 BuildItem clone = items[index].VirtualClone();
279                 items[index] = clone;
280 
281                 foreach (KeyValuePair<string, string> pair in metadata)
282                 {
283                     clone.SetVirtualMetadata(pair.Key, pair.Value);
284                 }
285 
286                 return clone;
287             }
288 
289             return null;
290         }
291 
292         /// <summary>
293         /// Applies each of the item modifications in order.
294         /// Items are NOT cloned.
295         /// Metadata is set as virtual metadata, so it is reset by Project.ResetBuildStatus().
296         /// If an item does not exist in this group, the modification is skipped.
297         /// If any modifications conflict, these modifications win.
298         /// </summary>
ModifyItemsUsingVirtualMetadata(Dictionary<BuildItem, Dictionary<string, string>> modifies)299         internal void ModifyItemsUsingVirtualMetadata(Dictionary<BuildItem, Dictionary<string, string>> modifies)
300         {
301             foreach (KeyValuePair<BuildItem, Dictionary<string, string>> modify in modifies)
302             {
303                 int index = items.IndexOf(modify.Key);
304                 if (index > -1)
305                 {
306                     foreach (KeyValuePair<string, string> pair in modify.Value)
307                     {
308                         items[index].SetVirtualMetadata(pair.Key, pair.Value);
309                     }
310                 }
311             }
312         }
313 
314         /// <summary>
315         /// Pre-allocates space in the item list.
316         /// PERF: Call this first before adding a known quantity of items to a group, to avoid
317         /// repeated expansions of the backing list.
318         /// </summary>
EnsureCapacity(int capacity)319         internal void EnsureCapacity(int capacity)
320         {
321             // Don't reduce capacity here; that's a list copy, too
322             if (capacity > items.Capacity)
323             {
324                 items.Capacity = capacity;
325             }
326         }
327 
328         /// <summary>
329         /// Adds an existing BuildItem to the list of items, does not attempt to add
330         /// backing Xml for the item.
331         /// </summary>
AddExistingItem(BuildItem itemToAdd)332         internal void AddExistingItem(BuildItem itemToAdd)
333         {
334             AddExistingItemAt(items.Count, itemToAdd);
335         }
336 
337         /// <summary>
338         /// Adds an existing BuildItem to the list of items at the specified index.
339         /// Does not attempt to add backing Xml for the item.
340         /// </summary>
AddExistingItemAt(int index, BuildItem itemToAdd)341         internal void AddExistingItemAt(int index, BuildItem itemToAdd)
342         {
343             ErrorUtilities.VerifyThrow(items != null, "BuildItemGroup has not been initialized.");
344             ErrorUtilities.VerifyThrow(index <= items.Count, "Index out of range");
345 
346             items.Insert(index, itemToAdd);
347 
348             if (parentProject != null)
349             {
350                 itemToAdd.ItemDefinitionLibrary = parentProject.ItemDefinitionLibrary;
351             }
352 
353             // If this BuildItemGroup is a persisted <ItemGroup>, then we need the
354             // items to have a reference back to their parent BuildItemGroup.  This
355             // makes it *much* easier to delete items through the object model.
356             if (IsPersisted)
357             {
358                 itemToAdd.ParentPersistedItemGroup = this;
359             }
360             MarkItemGroupAsDirty();
361         }
362 
363         /// <summary>
364         /// Adds an BuildItem to this BuildItemGroup.  If this is a persisted BuildItemGroup, then
365         /// this method also inserts the BuildItem's XML into the appropriate location
366         /// in the XML document.  For persisted ItemGroups, the behavior is that
367         /// it tries to insert the new BuildItem such that it is "near" other items of the
368         /// same type.  ("Near" is defined as just after the last existing item
369         /// of the same type, or at the end if none is found.)
370         /// </summary>
AddItem(BuildItem itemToAdd)371         internal void AddItem(BuildItem itemToAdd)
372         {
373             MustBeInitialized();
374 
375             // If we are a persisted <ItemGroup>, add the item element as a new child.
376             if (IsPersisted)
377             {
378                 MustNotBeImported();
379 
380                 // Make sure the item to be added has an XML element backing it,
381                 // and that its XML belongs to the same XML document as our BuildItemGroup.
382                 ErrorUtilities.VerifyThrow(itemToAdd.IsBackedByXml, "Item does not have an XML element");
383                 ErrorUtilities.VerifyThrow(itemToAdd.ItemElement.OwnerDocument == xml.OwnerDocument, "Cannot add an Item with a different XML owner document.");
384 
385                 // Generally, the desired behavior is to keep items of the same Type physically
386                 // contiguous within the BuildItemGroup.  (It's just easier to read that way.)  So we
387                 // scan through the existing items in our BuildItemGroup, and try to find the spot where
388                 // the new item would fit in alphabetically.  This is nice because it helps
389                 // source code control scenarios where multiple clients are adding items to
390                 // the same list.  By putting them in alphabetical order, there's less of a
391                 // chance of merge conflicts.
392                 int insertionIndex = items.Count;
393                 for (int i = 0; i < items.Count; i++)
394                 {
395                     if ( 0 == String.Compare(itemToAdd.Name, items[i].Name, StringComparison.OrdinalIgnoreCase))
396                     {
397                         insertionIndex = i + 1;
398 
399                         if ( 0 > String.Compare(itemToAdd.Include, items[i].Include, StringComparison.OrdinalIgnoreCase))
400                         {
401                             insertionIndex = i;
402                             break;
403                         }
404                     }
405                 }
406 
407                 // If there is at least one item in this group, then insert this new item
408                 // at the correct location based on our simple scan for similar item types.
409                 if (items.Count > 0)
410                 {
411                     if (insertionIndex == items.Count)
412                     {
413                         XmlElement itemElementToInsertAfter = items[items.Count - 1].ItemElement;
414                         xml.InsertAfter((XmlElement)itemElementToInsertAfter.ParentNode, itemToAdd.ItemElement, itemElementToInsertAfter);
415                     }
416                     else
417                     {
418                         XmlElement itemElementToInsertBefore = items[insertionIndex].ItemElement;
419                         xml.InsertBefore((XmlElement)itemElementToInsertBefore.ParentNode, itemToAdd.ItemElement, itemElementToInsertBefore);
420                     }
421 
422                     AddExistingItemAt(insertionIndex, itemToAdd);
423                 }
424                 else
425                 {
426                     // This BuildItemGroup is currently empty.
427                     // add the new BuildItem as a child of the <ItemGroup> tag.
428                     xml.AppendChild(itemToAdd.ItemElement);
429 
430                     // Add the item to the end of our list.
431                     AddExistingItem(itemToAdd);
432                 }
433             }
434             else
435             {
436                 // Add the item to the end of our list.
437                 AddExistingItem(itemToAdd);
438             }
439         }
440 
441         /// <summary>
442         /// Creates a new BuildItem defined by the given "Type" and "Include", and
443         /// adds it to the end of this BuildItemGroup.
444         /// If the group is persisted, the item is persisted; otherwise it is virtual
445         /// </summary>
AddNewItem(string itemName, string itemInclude)446         public BuildItem AddNewItem(string itemName, string itemInclude)
447         {
448             BuildItem newItem;
449 
450             if (IsPersisted)
451             {
452                 // We are a persisted <ItemGroup>, so create a new persisted item object.
453                 newItem = new BuildItem(xml.OwnerDocument, itemName, itemInclude, parentProject.ItemDefinitionLibrary);
454             }
455             else
456             {
457                 // Create a new virtual BuildItem.
458                 newItem = new BuildItem(itemName, itemInclude);
459             }
460 
461             AddItem(newItem);
462             return newItem;
463         }
464 
465         /// <summary>
466         /// Adds a new item to the ItemGroup, optional treating the item Include as literal so that
467         /// any special characters will be escaped before persisting it.
468         /// </summary>
AddNewItem(string itemName, string itemInclude, bool treatItemIncludeAsLiteral)469         public BuildItem AddNewItem(string itemName, string itemInclude, bool treatItemIncludeAsLiteral)
470         {
471             return AddNewItem(itemName, treatItemIncludeAsLiteral ? EscapingUtilities.Escape(itemInclude) : itemInclude);
472         }
473 
474         /// <summary>
475         /// Removes the given BuildItem from this BuildItemGroup.
476         /// If the item is part of the project manifest (ie, it's declared outside of a target) then
477         /// makes a backup of persisted items so that later the item group can be reverted to that backup,
478         /// reversing this change.
479         /// </summary>
RemoveItemWithBackup(BuildItem itemToRemove)480         internal void RemoveItemWithBackup(BuildItem itemToRemove)
481         {
482             MustBeInitialized();
483 
484             if (itemToRemove.IsPartOfProjectManifest)
485             {
486                 // We're about to remove an item that's part of the project manifest;
487                 // this must be reverted when we reset the project, so make sure we've got a backup
488                 BackupPersistedItems();
489             }
490 
491             // Don't remove the XML node, or mark the itemgroup as dirty; this is
492             // strictly an operation on temporary items, because we'll be un-backing up the
493             // persisted items at the end of the build
494 
495             items.Remove(itemToRemove);
496         }
497 
498         /// <summary>
499         /// Removes the given BuildItem from this BuildItemGroup.
500         /// If item is not in this group, does nothing.
501         /// </summary>
RemoveItem(BuildItem itemToRemove)502         public void RemoveItem(BuildItem itemToRemove)
503         {
504             MustBeInitialized();
505             RemoveItemElement(itemToRemove);
506             items.Remove(itemToRemove);
507             MarkItemGroupAsDirty();
508         }
509 
510         /// <summary>
511         /// Removes the item at the specified index.
512         /// </summary>
513         /// <exception cref="ArgumentOutOfRangeException">If index is out of bounds</exception>
RemoveItemAt(int index)514         public void RemoveItemAt(int index)
515         {
516             MustBeInitialized();
517             BuildItem item = items[index];
518             RemoveItemElement(item);
519             items.RemoveAt(index);
520             MarkItemGroupAsDirty();
521         }
522 
523         /// <summary>
524         /// If this is a persisted group, removes the XML element corresponding to the given item.
525         /// If this is not a persisted group, does nothing.
526         /// </summary>
RemoveItemElement(BuildItem item)527         private void RemoveItemElement(BuildItem item)
528         {
529             if (IsPersisted)
530             {
531                 MustNotBeImported();
532                 MustHaveThisParentElement(item);
533                 MustHaveThisParentGroup(item);
534                 xml.Element.RemoveChild(item.ItemElement);
535                 item.ParentPersistedItemGroup = null;
536             }
537         }
538 
539         /// <summary>
540         /// Clones the BuildItemGroup.  A shallow clone here is one that references
541         /// the same BuildItem objects as the original, whereas a deep clone actually
542         /// clones the BuildItem objects as well.  If this is a persisted BuildItemGroup,
543         /// only deep clones are allowed, because you can't have the same XML
544         /// element belonging to two parents.
545         /// </summary>
Clone(bool deepClone)546         public BuildItemGroup Clone(bool deepClone)
547         {
548             BuildItemGroup clone;
549 
550             if (IsPersisted)
551             {
552                 // Only deep clones are permitted when dealing with a persisted <ItemGroup>.
553                 // This is because a shallow clone would attempt to add the same item
554                 // elements to two different parent <ItemGroup> elements, and this is
555                 // not allowed.
556                 ErrorUtilities.VerifyThrowInvalidOperation(deepClone, "ShallowCloneNotAllowed");
557 
558                 // Do not set the ParentProject on the cloned BuildItemGroup, because it isn't really
559                 // part of the project.
560                 clone = new BuildItemGroup(xml.OwnerDocument, importedFromAnotherProject, null);
561                 clone.Condition = Condition;
562             }
563             else
564             {
565                 clone = new BuildItemGroup();
566             }
567 
568             // Loop through every BuildItem in our collection, and add those same Items
569             // to the cloned collection.
570 
571             clone.EnsureCapacity(this.Count); // PERF: important to pre-size
572 
573             foreach (BuildItem item in this)
574             {
575                 // If the caller requested a deep clone, then clone the BuildItem object,
576                 // and add the new BuildItem to the new BuildItemGroup.  Otherwise, just add
577                 // a reference to the existing BuildItem object to the new BuildItemGroup.
578                 clone.AddItem(deepClone ? item.Clone() : item);
579             }
580 
581             return clone;
582         }
583 
584         /// <summary>
585         /// Does a shallow clone, creating a new group with pointers to the same items as the old group.
586         /// </summary>
ShallowClone()587         internal BuildItemGroup ShallowClone()
588         {
589             return Clone(false /* shallow */);
590         }
591 
592         /// <summary>
593         /// Removes all Items from this BuildItemGroup, and also deletes the Condition
594         /// and Name.
595         /// </summary>
Clear()596         public void Clear()
597         {
598             MustBeInitialized();
599 
600             if (IsPersisted)
601             {
602                 MustNotBeImported();
603 
604                 foreach(BuildItem itemToRemove in items)
605                 {
606                     XmlElement itemElement = itemToRemove.ItemElement;
607                     MustHaveThisParentElement(itemToRemove);
608 
609                     itemElement.ParentNode.RemoveChild(itemElement);
610                     itemToRemove.ParentPersistedItemGroup = null;
611                 }
612 
613                 xml.Condition = null;
614             }
615 
616             items.Clear();
617             MarkItemGroupAsDirty();
618         }
619 
620         /// <summary>
621         /// Removes all virtual (intermediate) items from this BuildItemGroup.  This
622         /// is used to reset the state of the build back to the initial state,
623         /// when we only knew about the items that were actually declared in the
624         /// project XML.
625         /// </summary>
626         /// <owner>RGoel</owner>
RemoveAllIntermediateItems()627         internal void RemoveAllIntermediateItems()
628         {
629             MustBeInitialized();
630             MustBeVirtual("InvalidInPersistedItemGroup");
631 
632             if (IsBackedUp)
633             {
634                 // Revert removes of persisted items
635                 items = persistedItemBackup;
636                 persistedItemBackup = null;
637             }
638             else
639             {
640                 // Delete all virtual (those without XML backing) items.
641                 List<BuildItem> itemsToKeep = new List<BuildItem>(items.Count);
642                 for (int i = 0; i < items.Count; i++)
643                 {
644                     if (items[i].IsPartOfProjectManifest)
645                     {
646                         itemsToKeep.Add(items[i]);
647                     }
648                 }
649                 items = itemsToKeep;
650             }
651 
652             // Revert changes to persisted items' metadata
653             for (int i = 0; i < items.Count; i++)
654             {
655                 items[i].RevertToPersistedMetadata();
656             }
657 
658             MarkItemGroupAsDirty();
659         }
660 
661         /// <summary>
662         /// Marks the parent project as dirty.
663         /// </summary>
MarkItemGroupAsDirty()664         private void MarkItemGroupAsDirty()
665         {
666             if (parentProject != null)
667             {
668                 parentProject.MarkProjectAsDirty();
669             }
670         }
671 
672         /// <summary>
673         /// Create a secret backup list of our persisted items only.
674         /// Then, we can revert back to this later when we're done with the build,
675         /// and we want to remove any virtual items and revert any removes of
676         /// persisted items.
677         /// </summary>
BackupPersistedItems()678         internal void BackupPersistedItems()
679         {
680             if (!IsBackedUp)
681             {
682                 persistedItemBackup = new List<BuildItem>();
683 
684                 foreach (BuildItem item in items)
685                 {
686                     if (item.IsPartOfProjectManifest)
687                     {
688                         BuildItem itemClone = item.Clone();
689                         persistedItemBackup.Add(itemClone);
690                     }
691                 }
692             }
693         }
694 
695         /// <summary>
696         /// Call this method to verify that this item group is a well-formed
697         /// virtual item group.
698         /// </summary>
MustBeVirtual(string errorResourceName)699         private void MustBeVirtual(string errorResourceName)
700         {
701             ErrorUtilities.VerifyThrowInvalidOperation(!IsPersisted, errorResourceName);
702         }
703 
704         /// <summary>
705         /// Returns whether this is a persisted group.
706         /// </summary>
707         internal bool IsPersisted
708         {
709             get { return isPersisted; }
710         }
711 
712         /// <summary>
713         /// Returns whether the persisted items have been backed up for later
714         /// recovery.
715         /// </summary>
716         internal bool IsBackedUp
717         {
718             get { return (persistedItemBackup != null); }
719         }
720 
721         /// <summary>
722         /// Verifies this is a persisted group.
723         /// </summary>
MustBePersisted(string errorResourceName)724         private void MustBePersisted(string errorResourceName)
725         {
726             ErrorUtilities.VerifyThrowInvalidOperation(IsPersisted, errorResourceName);
727         }
728 
729         /// <summary>
730         /// Verifies this is not an imported item group.
731         /// </summary>
MustNotBeImported()732         private void MustNotBeImported()
733         {
734             ErrorUtilities.VerifyThrowInvalidOperation(!importedFromAnotherProject, "CannotModifyImportedProjects");
735         }
736 
737         /// <summary>
738         /// Verifies that the list of items has been created.
739         /// </summary>
MustBeInitialized()740         private void MustBeInitialized()
741         {
742             ErrorUtilities.VerifyThrow(this.items != null, "BuildItemGroup has not been initialized.");
743         }
744 
745         /// <summary>
746         /// Verifies that the item's parent element is indeed this item group's element.
747         /// </summary>
MustHaveThisParentElement(BuildItem item)748         private void MustHaveThisParentElement(BuildItem item)
749         {
750             ErrorUtilities.VerifyThrowInvalidOperation(item != null && item.ItemElement != null && item.ItemElement.ParentNode == xml.Element, "ItemDoesNotBelongToItemGroup");
751         }
752 
753         /// <summary>
754         /// Verifies the parent item group is indeed this item group.
755         /// </summary>
756         /// <param name="item"></param>
MustHaveThisParentGroup(BuildItem item)757         private void MustHaveThisParentGroup(BuildItem item)
758         {
759             ErrorUtilities.VerifyThrow(item.ParentPersistedItemGroup == this, "This item doesn't belong to this itemgroup");
760         }
761 
762         /// <summary>
763         /// Evaluates an item group that's *outside* of a Target.
764         /// Metadata is not allowed on conditions, and we against the parent project.
765         /// </summary>
Evaluate( BuildPropertyGroup existingProperties, Hashtable existingItemsByName, bool collectItemsIgnoringCondition, bool collectItemsRespectingCondition, ProcessingPass pass )766         internal void Evaluate
767         (
768             BuildPropertyGroup existingProperties,
769             Hashtable existingItemsByName,
770             bool collectItemsIgnoringCondition,
771             bool collectItemsRespectingCondition,
772             ProcessingPass pass
773         )
774         {
775             ErrorUtilities.VerifyThrow(pass == ProcessingPass.Pass2, "Pass should be Pass2 for ItemGroups.");
776             ErrorUtilities.VerifyThrow(collectItemsIgnoringCondition || collectItemsRespectingCondition, "collectItemsIgnoringCondition and collectItemsRespectingCondition can't both be false.");
777 
778             Expander expander = new Expander(existingProperties, existingItemsByName, ExpanderOptions.ExpandAll);
779 
780             bool itemGroupCondition = Utilities.EvaluateCondition(Condition,
781                                                          (IsPersisted ? xml.ConditionAttribute : null),
782                                                          expander,
783                                                          ParserOptions.AllowPropertiesAndItemLists,
784                                                          parentProject);
785 
786             if (!itemGroupCondition && !collectItemsIgnoringCondition)
787             {
788                 // Neither list needs updating
789                 return;
790             }
791 
792             foreach (BuildItem currentItem in this)
793             {
794                 bool itemCondition = Utilities.EvaluateCondition(currentItem.Condition,
795                                                              currentItem.ConditionAttribute,
796                                                              expander,
797                                                              ParserOptions.AllowPropertiesAndItemLists,
798                                                              parentProject);
799 
800                 if (!itemCondition && !collectItemsIgnoringCondition)
801                 {
802                     // Neither list needs updating
803                     continue;
804                 }
805 
806                 if (collectItemsIgnoringCondition)
807                 {
808                     // Since we're re-evaluating the project, clear out the previous list of child items
809                     // for each persisted item tag.
810                     currentItem.ChildItems.Clear();
811                 }
812 
813                 currentItem.EvaluateAllItemMetadata(expander, ParserOptions.AllowPropertiesAndItemLists, parentProject.ParentEngine.LoggingServices, parentProject.ProjectBuildEventContext);
814                 BuildItemGroup items = BuildItemGroup.ExpandItemIntoItems(parentProject.ProjectDirectory, currentItem, expander, false /* do not expand metadata */);
815 
816                 foreach (BuildItem item in items)
817                 {
818                     BuildItem newItem = BuildItem.CreateClonedParentedItem(item, currentItem);
819 
820                     if (itemGroupCondition && itemCondition && collectItemsRespectingCondition)
821                     {
822                         parentProject.AddToItemListByName(newItem);
823                     }
824 
825                     if (collectItemsIgnoringCondition)
826                     {
827                         parentProject.AddToItemListByNameIgnoringCondition(newItem);
828 
829                         // Set up the other half of the parent/child relationship.
830                         newItem.ParentPersistedItem.ChildItems.AddItem(newItem);
831                     }
832                 }
833             }
834         }
835 
836         /// <summary>
837         /// Processes the "include" list and the "exclude" list for an item element, and returns
838         /// the resultant virtual group of items. Ignores any condition on the item.
839         /// </summary>
840         /// <param name="baseDirectory">Where relative paths should be evaluated from</param>
841         /// <param name="originalItem">The "mother" item that's being expanded</param>
842         /// <param name="expander">Expander to evaluated items and properties</param>
843         /// <param name="expandMetadata">Whether metadata expressions should be expanded, or left as literals</param>
ExpandItemIntoItems( string baseDirectory, BuildItem originalItem, Expander expander, bool expandMetadata )844         internal static BuildItemGroup ExpandItemIntoItems
845         (
846             string baseDirectory,
847             BuildItem originalItem,
848             Expander expander,
849             bool expandMetadata
850         )
851         {
852             ErrorUtilities.VerifyThrow(originalItem != null, "Can't add a null item to project.");
853             ErrorUtilities.VerifyThrow(expander != null, "expander can't be null.");
854 
855             // Take the entire string specified in the "Include" attribute, and split
856             // it up by semi-colons.  Then loop over the individual pieces.
857             // Expand only with properties first, so that expressions like Include="@(foo)" will transfer the metadata of the "foo" items as well, not just their item specs.
858             List<string> itemIncludePieces = (new Expander(expander, ExpanderOptions.ExpandProperties).ExpandAllIntoStringListLeaveEscaped(originalItem.Include, originalItem.IncludeAttribute));
859             BuildItemGroup itemsToInclude = new BuildItemGroup();
860             for (int i = 0; i < itemIncludePieces.Count; i++)
861             {
862                 BuildItemGroup itemizedGroup = expander.ExpandSingleItemListExpressionIntoItemsLeaveEscaped(itemIncludePieces[i], originalItem.IncludeAttribute);
863                 if (itemizedGroup == null)
864                 {
865                     // The expression did not represent a single @(...) item list reference.
866                     if (expandMetadata)
867                     {
868                         // We're inside a target: metadata expressions like %(foo) are legal, so expand them now
869                         itemIncludePieces[i] = expander.ExpandMetadataLeaveEscaped(itemIncludePieces[i]);
870                     }
871                     // Now it's a string constant, possibly with wildcards.
872                     // Take each individual path or file expression, and expand any
873                     // wildcards.  Then loop through each file returned.
874                     if (itemIncludePieces[i].Length > 0)
875                     {
876                         string[] includeFileList = EngineFileUtilities.GetFileListEscaped(baseDirectory, itemIncludePieces[i]);
877                         for (int j = 0; j < includeFileList.Length; j++)
878                         {
879                             BuildItem newItem = itemsToInclude.AddNewItem(originalItem.Name, originalItem.Include);
880                             newItem.SetEvaluatedItemSpecEscaped(itemIncludePieces[i]);   // comes from XML include --- "arbitrarily escaped"
881                             newItem.SetFinalItemSpecEscaped(includeFileList[j]);  // comes from file system matcher -- "canonically escaped"
882                         }
883                     }
884                 }
885                 else
886                 {
887                     itemsToInclude.ImportItems(itemizedGroup);
888                 }
889             }
890 
891             List<BuildItem> matchingItems = FindItemsMatchingSpecification(itemsToInclude, originalItem.Exclude, originalItem.ExcludeAttribute, expander, baseDirectory);
892 
893             if (matchingItems != null)
894             {
895                 foreach (BuildItem item in matchingItems)
896                 {
897                     itemsToInclude.RemoveItem(item);
898                 }
899             }
900 
901             return itemsToInclude;
902         }
903 
904         /// <summary>
905         /// Returns a list of all items in the provided item group whose itemspecs match the specification, after it is split and any wildcards are expanded.
906         /// If not items match, returns null.
907         /// </summary>
FindItemsMatchingSpecification(BuildItemGroup items, string specification, XmlAttribute attribute, Expander expander, string baseDirectory)908         internal static List<BuildItem> FindItemsMatchingSpecification(BuildItemGroup items, string specification, XmlAttribute attribute, Expander expander, string baseDirectory)
909         {
910             if (items.Count == 0 || specification.Length == 0)
911             {
912                 return null;
913             }
914 
915             // This is a hashtable whose key is the filename for the individual items
916             // in the Exclude list, after wildcard expansion.  The value in the hash table
917             // is just an empty string.
918             Hashtable specificationsToFind = new Hashtable(StringComparer.OrdinalIgnoreCase);
919 
920             // Split by semicolons
921             List<string> specificationPieces = expander.ExpandAllIntoStringListLeaveEscaped(specification, attribute);
922 
923             foreach (string piece in specificationPieces)
924             {
925                 // Take each individual path or file expression, and expand any
926                 // wildcards.  Then loop through each file returned, and add it
927                 // to our hashtable.
928 
929                 // Don't unescape wildcards just yet - if there were any escaped, the caller wants to treat them
930                 // as literals. Everything else is safe to unescape at this point, since we're only matching
931                 // against the file system.
932                 string[] fileList = EngineFileUtilities.GetFileListEscaped(baseDirectory, piece);
933 
934                 foreach (string file in fileList)
935                 {
936                     // Now unescape everything, because this is the end of the road for this filename.
937                     // We're just going to compare it to the unescaped include path to filter out the
938                     // file excludes.
939                     specificationsToFind[EscapingUtilities.UnescapeAll(file)] = String.Empty;
940                 }
941             }
942 
943             if (specificationsToFind.Count == 0)
944             {
945                 return null;
946             }
947 
948             // Now loop through our list and filter out any that match a
949             // filename in the remove list.
950             List<BuildItem> itemsRemoved = new List<BuildItem>();
951 
952             foreach (BuildItem item in items)
953             {
954                 // Even if the case for the excluded files is different, they
955                 // will still get excluded, as expected.  However, if the excluded path
956                 // references the same file in a different way, such as by relative
957                 // path instead of absolute path, we will not realize that they refer
958                 // to the same file, and thus we will not exclude it.
959                 if (specificationsToFind.ContainsKey(item.FinalItemSpec))
960                 {
961                     itemsRemoved.Add(item);
962                 }
963             }
964 
965             return itemsRemoved;
966         }
967 
968         #endregion
969     }
970 }
971