|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// THE ASSEMBLY BUILT FROM THIS SOURCE FILE HAS BEEN DEPRECATED FOR YEARS. IT IS BUILT ONLY TO PROVIDE
// BACKWARD COMPATIBILITY FOR API USERS WHO HAVE NOT YET MOVED TO UPDATED APIS. PLEASE DO NOT SEND PULL
// REQUESTS THAT CHANGE THIS FILE WITHOUT FIRST CHECKING WITH THE MAINTAINERS THAT THE FIX IS REQUIRED.
using System;
using System.Xml;
using System.Diagnostics;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.BuildEngine.Shared;
namespace Microsoft.Build.BuildEngine
{
/// <summary>
/// This class represents a single item of the project. An item is usually a file on disk, with a type associated with it, and
/// its own item-specific attributes. The list of items is initially specified via XML tags in the project file, although a
/// single item tag can represent multiple items through the use of standard wilcards * and ?. Also, tasks can add new items
/// of various types to the project's item list -- these items don't have any XML representation.
/// </summary>
/// <owner>RGoel</owner>
[DebuggerDisplay("BuildItem (Name = { Name }, Include = { Include }, FinalItemSpec = { FinalItemSpec }, Condition = { Condition } )")]
public class BuildItem
{
#region Member Data
// Whether the item is part of the project "manifest" - ie, in the project outside of a target.
// Note that items specified inside of targets may have backing XML, but aren't considered persisted.
private bool isPartOfProjectManifest;
// Object holding the backing Xml, if any
// For virtual items (i.e., those generated by tasks), this is null.
private BuildItemGroupChildXml xml;
// This is the "type" of the item, for example, "CPPFile". This has nothing to
// do with the file extension or anything else related to the OS.
// Each project author can define his own item types, which are
// just a way of bucketing similar items together.
private string name;
/// <summary>
/// The library to consult for possible default metadata values when the
/// item itself does not have a value for requested metadata.
/// May be null, if this item is not associated with a particular project.
/// </summary>
private ItemDefinitionLibrary itemDefinitionLibrary;
// This is the "Include" value of a virtual item. This
// may contain wildcards.
// For persisted (non-virtual) items, the include is gotten from
// the backing xml; there is no point storing a copy here, and this
// remains null.
private string include = null;
// In the project file, the user can put arbitrary meta-data on the item tag. These meta-data are termed "custom item
// attributes" (NOT to be confused with XML attributes on the item element). For a persisted BuildItem object, these meta-data
// are stored as child nodes of the "itemElement", and are also cached in this hash table (to allow case-insensitive
// lookup). For a virtual BuildItem object, the meta-data are only stored in this hash table.
// NOTE: for a persisted item, it is possible for the same custom attribute to appear multiple times under an item
// element -- we allow this, but follow a "last one wins" policy
private CopyOnWriteHashtable unevaluatedCustomMetadata = null;
private CopyOnWriteHashtable evaluatedCustomMetadata = null;
// Private copies of the above metadata tables, kept if the metadata has been modified during the build;
// we can revert to them later to go back to the pre-build state
private CopyOnWriteHashtable unevaluatedCustomMetadataBackup;
private CopyOnWriteHashtable evaluatedCustomMetadataBackup;
// If this item is persisted in the project file, then we need to
// store a reference to the parent <ItemGroup>. This makes it easier
// to remove an item from a project through the object model.
private BuildItemGroup parentPersistedItemGroup = null;
// When an item in a project file gets evaluated, it may evaluate to
// several different items. For example, in the project file,
// <Blah Include="a;b;c"/> really evaluates to 3 separate
// items: a, b, and c.
// For each of these 3 evaluated items, the parent item is the original
// item tag that came from the project file. When one of these
// "child" items is modified/deleted/etc. through the object model, we
// need a pointer to the parent item in order to modify it accordingly.
private BuildItem parentPersistedItem = null;
// When an item in a project file gets evaluated, it may evaluate to
// several different items. This is the list of child items that
// a real item tag in the project file evaluated to.
private BuildItemGroup childItems = null;
// this is the Include attribute with all embedded properties expanded, but with wildcards intact
private string evaluatedItemSpecEscaped;
// This is the final evaluated item specification, and is only valid
// if you are looking at an item returned from Project.EvaluatedItems.
// If the "Include" attribute was something simple that represented a
// single file, like "foo.cs", then this finalItemSpec would also be
// "foo.cs". However, in the case of wildcards, the "Include" attribute
// might say "*.cs", whereas this finalItemSpec would be one single item
// such as "foo.cs".
private string finalItemSpecEscaped = String.Empty;
// this table is used to cache the results of path manipulations performed on the (evaluated/final) item-spec if the
// item-spec is a true file-spec i.e. it contains file or directory path information
// NOTE: modifications to the item-spec are typically needed for item vector transforms, but the modifiers are also
// exposed as pre-defined/reserved attributes on the item
private Hashtable itemSpecModifiers;
// the portion of the directory of the final item-spec that was generated by a recursive wildcard
private string recursivePortionOfFinalItemSpecDirectory = null;
// If this is a persisted item element, this boolean tells us whether
// it came from the main project file or an imported project file.
private bool importedFromAnotherProject = false;
#endregion
#region CustomSerializationToStream
internal void WriteToStream(BinaryWriter writer)
{
writer.Write(importedFromAnotherProject);
#region RecursivePortionOfFinalItemSpecDirectory
if (recursivePortionOfFinalItemSpecDirectory == null)
{
writer.Write((byte)0);
}
else
{
writer.Write((byte)1);
writer.Write(recursivePortionOfFinalItemSpecDirectory);
}
#endregion
#region FinalItemSpecEscaped
if (finalItemSpecEscaped == null)
{
writer.Write((byte)0);
}
else
{
writer.Write((byte)1);
writer.Write(finalItemSpecEscaped);
}
#endregion
#region Name
if (name == null)
{
writer.Write((byte)0);
}
else
{
writer.Write((byte)1);
writer.Write(name);
}
#endregion
#region Include
string includeValue = this.include;
if (IsBackedByXml)
{
includeValue = xml.Include;
}
if (includeValue == null)
{
writer.Write((byte)0);
}
else
{
writer.Write((byte)1);
writer.Write(includeValue);
}
#endregion
#region EvaluatedItemSpecEscaped
if (evaluatedItemSpecEscaped == null)
{
writer.Write((byte)0);
}
else
{
writer.Write((byte)1);
writer.Write(evaluatedItemSpecEscaped);
}
#endregion
#region UnevaluatedCustomMetaData
IDictionary metadata = GetAllCustomUnevaluatedMetadata();
if (metadata == null)
{
writer.Write((byte)0);
}
else
{
writer.Write((byte)1);
writer.Write((Int32)metadata.Count);
foreach (string key in metadata.Keys)
{
writer.Write(key);
if (metadata[key] == null)
{
writer.Write((byte)0);
}
else
{
writer.Write((byte)1);
writer.Write((string)metadata[key]);
}
}
}
#endregion
#region EvaluatedCustomMetaData
metadata = GetAllCustomEvaluatedMetadata();
if (metadata == null)
{
writer.Write((byte)0);
}
else
{
writer.Write((byte)1);
writer.Write((Int32)metadata.Count);
foreach (string key in metadata.Keys)
{
writer.Write(key);
if (metadata[key] == null)
{
writer.Write((byte)0);
}
else
{
writer.Write((byte)1);
writer.Write((string)metadata[key]);
}
}
}
#endregion
#region ItemSpecModifiers
if (itemSpecModifiers == null)
{
writer.Write((byte)0);
}
else
{
writer.Write((byte)1);
writer.Write((Int32)itemSpecModifiers.Count);
foreach (string key in itemSpecModifiers.Keys)
{
writer.Write(key);
if (itemSpecModifiers[key] == null)
{
writer.Write((byte)0);
}
else
{
writer.Write((byte)1);
writer.Write((string)itemSpecModifiers[key]);
}
}
}
#endregion
}
internal void CreateFromStream(BinaryReader reader)
{
importedFromAnotherProject = reader.ReadBoolean();
#region RecursivePortionOfFinalItemSpecDirectory
if (reader.ReadByte() == 0)
{
recursivePortionOfFinalItemSpecDirectory = null;
}
else
{
recursivePortionOfFinalItemSpecDirectory = reader.ReadString();
}
#endregion
#region FinalItemSpecEscaped
if (reader.ReadByte() == 0)
{
finalItemSpecEscaped = null;
}
else
{
finalItemSpecEscaped = reader.ReadString();
}
#endregion
#region Name
if (reader.ReadByte() == 0)
{
name = null;
}
else
{
name = reader.ReadString();
}
#endregion
#region VitrualIncludeAttribute
if (reader.ReadByte() == 0)
{
include = null;
}
else
{
include = reader.ReadString();
}
#endregion
#region EvaluatedItemSpecEscaped
if (reader.ReadByte() == 0)
{
evaluatedItemSpecEscaped = null;
}
else
{
evaluatedItemSpecEscaped = reader.ReadString();
}
#endregion
#region UnevaluatedCustomMetadata
if (reader.ReadByte() == 0)
{
unevaluatedCustomMetadata = null;
}
else
{
int numberUnevaluatedItems = reader.ReadInt32();
unevaluatedCustomMetadata = new CopyOnWriteHashtable(numberUnevaluatedItems, StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < numberUnevaluatedItems; i++)
{
string key = reader.ReadString();
string value = null;
if (reader.ReadByte() != 0)
{
value = reader.ReadString();
}
unevaluatedCustomMetadata.Add(key, value);
}
}
#endregion
#region EvaluatedCustomMetadata
if (reader.ReadByte() == 0)
{
evaluatedCustomMetadata = null;
}
else
{
int numberevaluatedCustomMetadata = reader.ReadInt32();
evaluatedCustomMetadata = new CopyOnWriteHashtable(numberevaluatedCustomMetadata, StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < numberevaluatedCustomMetadata; i++)
{
string key = reader.ReadString();
string value = null;
if (reader.ReadByte() != 0)
{
value = reader.ReadString();
}
evaluatedCustomMetadata.Add(key, value);
}
}
#endregion
#region ItemSpecModifiers
if (reader.ReadByte() == 0)
{
itemSpecModifiers = null;
}
else
{
int numberItemSpecModifiers = reader.ReadInt32();
itemSpecModifiers = new Hashtable(numberItemSpecModifiers);
for (int i = 0; i < numberItemSpecModifiers; i++)
{
string key = reader.ReadString();
string value = null;
if (reader.ReadByte() != 0)
{
value = reader.ReadString();
}
itemSpecModifiers.Add(key, value);
}
}
#endregion
}
#endregion
#region Constructors
/// <summary>
/// Creates a new item with an XML element backing it. Use this to add a new persisted item to the project file.
/// </summary>
/// <param name="ownerDocument">can be null</param>
/// <param name="name">can be null</param>
internal BuildItem(XmlDocument ownerDocument, string name, string include, ItemDefinitionLibrary itemDefinitionLibrary)
: this(ownerDocument, name, include, true /* create custom metadata cache */, itemDefinitionLibrary)
{
}
/// <summary>
/// Creates either a new item with an XML element backing it, or a virtual
/// item. To conserve memory, this constructor does not allocate storage
/// for custom metadata, unless told to do so.
/// </summary>
/// <remarks>
/// PERF WARNING: Allocating memory for the custom metadata cache is expensive
/// when a build generates a large number of items.
/// </remarks>
/// <param name="ownerDocument">can be null</param>
/// <param name="name">can be null</param>
private BuildItem(XmlDocument ownerDocument, string name, string include, bool createCustomMetadataCache, ItemDefinitionLibrary itemDefinitionLibrary)
{
BuildItemHelper(ownerDocument, name, include, createCustomMetadataCache, itemDefinitionLibrary);
}
/// <summary>
/// Common code for constructors. If an ownerDocument is passed in, it's a persisted element.
/// </summary>
/// <param name="itemName">can be null</param>
/// <param name="itemDefinitionLibrary">can only be null if ownerDocument is null</param>
private void BuildItemHelper(XmlDocument ownerDocument, string itemName, string itemInclude, bool createCustomMetadataCache, ItemDefinitionLibrary itemDefinitionLibraryToUse)
{
// Only check for null. It's legal to make BuildItems with empty
// item specs -- this is to be consistent with how we shipped TaskItem.
// See #567058.
ErrorUtilities.VerifyThrowArgumentNull(itemInclude, nameof(itemInclude));
// Validate that the item name doesn't contain any illegal characters.
if (itemName != null)
{
XmlUtilities.VerifyThrowValidElementName(itemName);
ErrorUtilities.VerifyThrowInvalidOperation(XMakeElements.IllegalItemPropertyNames[itemName] == null, "CannotModifyReservedItem", itemName);
}
// If no owner document was passed in, then it's not going to have an
// XML element backing it.
if (ownerDocument == null)
{
this.include = itemInclude;
this.isPartOfProjectManifest = false;
}
else
{
ErrorUtilities.VerifyThrowArgumentLength(itemName, "itemType");
MustHaveItemDefinitionLibrary(itemDefinitionLibraryToUse);
// The caller has given us an owner document, so we're going to create a
// new item element associated with that document. The new item element
// doesn't actually get added to the XML document automatically.
this.xml = new BuildItemGroupChildXml(ownerDocument, itemName, itemInclude);
this.isPartOfProjectManifest = true;
}
if (createCustomMetadataCache)
{
// PERF NOTE: only create cache if told to do so, because creating
// this cache for a large number of items is expensive
InitializeCustomMetadataCache();
}
this.name = itemName;
// The evaluated and final item specs start off initialized to the "Include" attribute.
this.evaluatedItemSpecEscaped = itemInclude;
this.finalItemSpecEscaped = itemInclude;
this.importedFromAnotherProject = false;
this.itemDefinitionLibrary = itemDefinitionLibraryToUse;
}
/// <summary>
/// This constructor creates a new virtual (non-persisted) item with the
/// specified type and include.
/// </summary>
public BuildItem(string itemName, string itemInclude) :
this(null /* no XML */, itemName, itemInclude, null /* no item definition library */)
{
}
/// <summary>
/// This constructor initializes a persisted item from an existing item
/// element which exists either in the main project file or one of the
/// imported files.
/// </summary>
internal BuildItem(XmlElement itemElement, bool importedFromAnotherProject, ItemDefinitionLibrary itemDefinitionLibrary)
: this(itemElement, importedFromAnotherProject, true /* part of project manifest */, itemDefinitionLibrary)
{
}
/// <summary>
/// This constructor initializes an item from an item element.
/// It is part of the project manifest or not as specified.
/// </summary>
internal BuildItem(XmlElement itemElement, bool importedFromAnotherProject, bool isPartOfProjectManifest, ItemDefinitionLibrary itemDefinitionLibrary)
{
MustHaveItemDefinitionLibrary(itemDefinitionLibrary);
InitializeFromItemElement(itemElement);
this.importedFromAnotherProject = importedFromAnotherProject;
this.isPartOfProjectManifest = isPartOfProjectManifest;
this.itemDefinitionLibrary = itemDefinitionLibrary;
ProjectErrorUtilities.VerifyThrowInvalidProject(XMakeElements.IllegalItemPropertyNames[name] == null, ItemElement, "CannotModifyReservedItem", name);
}
/// <summary>
/// This constructor creates a new virtual (non-persisted) item based
/// on a ITaskItem object that was emitted by a task.
/// </summary>
public BuildItem(string itemName, ITaskItem taskItem)
{
ErrorUtilities.VerifyThrowArgumentNull(taskItem, nameof(taskItem));
string itemInclude = EscapingUtilities.Escape(taskItem.ItemSpec);
BuildItemHelper
(
null /* this is a virtual item with no backing XML */,
itemName,
itemInclude,
false, /* PERF NOTE: don't waste time creating a new custom metadata cache,
* because we're going to clone the given task item's custom metadata */
null /* no definition library */
);
IDictionary rawSourceTable = taskItem.CloneCustomMetadata();
// Go through and escape the metadata as necessary.
string[] keys = new string[rawSourceTable.Count];
rawSourceTable.Keys.CopyTo(keys, 0);
foreach (string singleMetadataName in keys)
{
string singleMetadataValue = (string)rawSourceTable[singleMetadataName];
rawSourceTable[singleMetadataName] = EscapingUtilities.Escape(singleMetadataValue);
}
this.unevaluatedCustomMetadata = new CopyOnWriteHashtable(rawSourceTable, StringComparer.OrdinalIgnoreCase);
this.evaluatedCustomMetadata = new CopyOnWriteHashtable(rawSourceTable, StringComparer.OrdinalIgnoreCase);
this.isPartOfProjectManifest = false;
}
#endregion
#region Properties
/// <summary>
/// This returns a boolean telling you whether this particular item
/// was imported from another project, or whether it was defined
/// in the main project. For virtual items which have no
/// persistence, this is false.
/// </summary>
public bool IsImported
{
get { return importedFromAnotherProject; }
}
/// <summary>
/// Accessor for the item's "type" string. Note that changing the "Type"
/// of an BuildItem requires the whole project to be re-evalauted. This is because
/// items are frequently stored in hash tables based on their item types,
/// so changing an item type would mess up the tables. In the current
/// implementation the caller who changes the item type is responsible
/// for calling Project.MarkAsDirty().
/// </summary>
public string Name
{
get
{
ErrorUtilities.VerifyThrow(name != null, "Get Name: Item has not been initialized.");
return this.name;
}
set
{
ErrorUtilities.VerifyThrow(name != null, "Set Name: Item has not been initialized.");
ErrorUtilities.VerifyThrowArgumentLength(value, "Name");
XmlUtilities.VerifyThrowValidElementName(value);
MustNotBeImported();
ErrorUtilities.VerifyThrowInvalidOperation(XMakeElements.IllegalItemPropertyNames[value] == null, "CannotModifyReservedItem", value);
SplitChildItemIfNecessary();
this.name = value;
if (IsBackedByXml)
{
xml.Name = value;
// Because we actually have a new XML element representing this item now, we
// have to update our parent items and child items. Most other modifications
// to an item don't require this, because they are simply modifying the XML
// element. But here, because the item type is the XML element name, we actually
// had to create a new XML element with the new item type. This was done by the
// RenameXmlElement method, called above.
if (this.ParentPersistedItem != null)
{
ParentPersistedItem.UpdateBackingXml(this.xml);
}
if (this.childItems != null)
{
foreach (BuildItem childItem in this.childItems)
{
childItem.UpdateBackingXml(this.xml);
}
}
}
MarkItemAsDirty();
}
}
/// <summary>
/// The backing library of default metadata values, if any.
/// Projects need to update this with their own library,
/// when an item is added to them.
/// </summary>
internal ItemDefinitionLibrary ItemDefinitionLibrary
{
get { return itemDefinitionLibrary; }
set { itemDefinitionLibrary = value; }
}
/// Accessor for the item's "include" string.
/// </summary>
/// <owner>RGoel</owner>
public string Include
{
get
{
if (IsBackedByXml)
{
return xml.Include;
}
else if (include != null)
{
return include;
}
else
{
ErrorUtilities.ThrowInternalError("Item has not been initialized.");
return null;
}
}
set
{
ErrorUtilities.VerifyThrowArgument(value != null, "NullIncludeNotAllowed", XMakeAttributes.include);
MustNotBeImported();
if (IsBackedByXml)
{
// If this is an evaluated item that originated from the project file, and the original
// item is declared using a wildcard that still matches the new item spec ...
if ((ParentPersistedItem?.NewItemSpecMatchesExistingWildcard(value) == true))
{
// Don't need to touch the project file since the original wildcard still matches
// the new item spec. But it still should be reevaluated the next time around.
MarkItemAsDirtyForReevaluation();
}
else
{
SplitChildItemIfNecessary();
xml.Include = value;
MarkItemAsDirty();
}
}
else if (this.include != null)
{
this.include = value;
MarkItemAsDirty();
}
else
{
ErrorUtilities.VerifyThrow(false, "Item has not been initialized.");
}
// The evaluated and final item specs start off initialized to the "Include" attribute.
this.evaluatedItemSpecEscaped = value;
this.finalItemSpecEscaped = value;
}
}
/// <summary>
/// Gets the names of metadata on the item -- also includes the pre-defined/reserved item-spec modifiers.
/// </summary>
/// <owner>SumedhK, JomoF</owner>
/// <value>Collection of name strings.</value>
public ICollection MetadataNames
{
get
{
// Add all the custom metadata.
List<string> list = GetAllCustomMetadataNames();
// Add all the built-in metadata.
list.AddRange(FileUtilities.ItemSpecModifiers.All);
return list;
}
}
/// <summary>
/// Gets the number of metadata set on the item.
/// </summary>
/// <owner>SumedhK</owner>
/// <value>Count of metadata.</value>
public int MetadataCount
{
get
{
return GetCustomMetadataCount() + FileUtilities.ItemSpecModifiers.All.Length;
}
}
/// <summary>
/// Gets the names of metadata on the item -- also includes the pre-defined/reserved item-spec modifiers.
/// </summary>
/// <owner>SumedhK, JomoF</owner>
/// <value>Collection of name strings.</value>
public ICollection CustomMetadataNames
{
get
{
// All the custom metadata.
return GetAllCustomMetadataNames();
}
}
/// <summary>
/// Gets the number of metadata set on the item.
/// </summary>
/// <owner>SumedhK</owner>
/// <value>Count of metadata.</value>
public int CustomMetadataCount
{
get
{
return GetCustomMetadataCount();
}
}
/// <summary>
/// Read-only accessor for accessing the XML attribute for "Include". Callers should
/// never try and modify this. Go through this.Include to change the include spec.
/// </summary>
internal XmlAttribute IncludeAttribute
{
get { return IsBackedByXml ? xml.IncludeAttribute : null; }
}
/// <summary>
/// Accessor for the item's "exclude" string.
/// </summary>
/// <owner>RGoel</owner>
public string Exclude
{
get { return IsBackedByXml ? xml.Exclude : String.Empty; }
set
{
// We don't support having an "Exclude" for virtual items. Only persisted items can have an "Exclude".
ErrorUtilities.VerifyThrowInvalidOperation(IsBackedByXml, "CannotSetExcludeOnVirtualItem", XMakeAttributes.exclude);
MustNotBeImported();
ErrorUtilities.VerifyThrowInvalidOperation(this.ParentPersistedItem == null, "CannotSetExcludeOnEvaluatedItem", XMakeAttributes.exclude);
xml.Exclude = value;
MarkItemAsDirty();
}
}
/// <summary>
/// Read-only accessor for accessing the XML attribute for "Exclude". Callers should
/// never try and modify this. Go through this.Exclude to change the exclude spec.
/// </summary>
/// <owner>RGoel</owner>
internal XmlAttribute ExcludeAttribute
{
get { return IsBackedByXml ? xml.ExcludeAttribute : null; }
}
/// <summary>
/// Accessor for the item's "condition".
/// </summary>
/// <owner>RGoel</owner>
public string Condition
{
get { return IsBackedByXml ? xml.Condition : String.Empty; }
set
{
// If this BuildItem object is not actually represented by an
// XML element in the project file, then do not allow
// the caller to set the condition.
ErrorUtilities.VerifyThrowInvalidOperation(IsBackedByXml, "CannotSetCondition");
MustNotBeImported();
SplitChildItemIfNecessary();
xml.Condition = value;
MarkItemAsDirty();
}
}
/// <summary>
/// Read-only accessor for accessing the XML attribute for "Condition". Callers should
/// never try and modify this. Go through this.Condition to change the condition.
/// </summary>
/// <owner>RGoel</owner>
internal XmlAttribute ConditionAttribute
{
get { return IsBackedByXml ? xml.ConditionAttribute : null; }
}
/// <summary>
/// Gets the XmlElement representing this item.
/// </summary>
/// <owner>RGoel</owner>
/// <value>The item XmlElement, or null if item is virtual.</value>
internal XmlElement ItemElement
{
get { return IsBackedByXml ? xml.Element : null; }
}
/// <summary>
/// Accessor for the final evaluated item specification. This is read-only.
/// </summary>
/// <owner>RGoel</owner>
internal string FinalItemSpecEscaped
{
get { return finalItemSpecEscaped; }
}
/// <summary>
/// Returns the unescaped final value of the item.
/// </summary>
/// <owner>RGoel</owner>
public string FinalItemSpec
{
get { return EscapingUtilities.UnescapeAll(FinalItemSpecEscaped); }
}
/// <summary>
/// Read-only accessor for the piece of the item's Include that resulted in
/// this item, with properties expanded.
/// </summary>
internal string EvaluatedItemSpec
{
get { return evaluatedItemSpecEscaped; }
}
/// <summary>
/// If this item is persisted in the project file, then we need to
/// store a reference to the parent <ItemGroup>. This makes it easier
/// to remove an item from a project through the object model.
/// </summary>
/// <owner>RGoel</owner>
internal BuildItemGroup ParentPersistedItemGroup
{
get { return parentPersistedItemGroup; }
set
{
ErrorUtilities.VerifyThrow(((value == null) && (this.parentPersistedItemGroup != null)) || ((value != null) && (this.parentPersistedItemGroup == null)),
"Either new parent cannot be assigned because we already have a parent, or old parent cannot be removed because none exists.");
this.parentPersistedItemGroup = value;
}
}
/// <summary>
/// When an item in a project file gets evaluated, it may evaluate to
/// several different items. For example, in the project file,
/// <Blah Include="a;b;c"/> really evaluates to 3 separate
/// items: a, b, and c.
/// For one of these 3 evaluated items, the parent item is the original
/// item tag that came from the project file. When one of these
/// "child" items is modified/deleted/etc. through the object model, we
/// need a pointer to the parent item in order to modify it accordingly.
/// </summary>
/// <owner>rgoel</owner>
internal BuildItem ParentPersistedItem
{
get { return parentPersistedItem; }
set
{
ErrorUtilities.VerifyThrow(((value == null) && (this.parentPersistedItem != null)) || ((value != null) && (this.parentPersistedItem == null)),
"Either new parent cannot be assigned because we already have a parent, or old parent cannot be removed because none exists.");
this.parentPersistedItem = value;
}
}
/// <summary>
/// When an item in a project file gets evaluated, it may evaluate to
/// several different items. This is the list of child items that
/// a real item tag in the project file evaluated to.
/// </summary>
/// <owner>RGoel</owner>
internal BuildItemGroup ChildItems
{
get
{
if (this.childItems == null)
{
this.childItems = new BuildItemGroup();
}
return this.childItems;
}
}
internal bool IsUninitializedItem
{
get { return this.name == null; }
}
/// <summary>
/// Whether this item is part of the project "manifest", ie., it is defined in XML
/// outside of a target.
/// </summary>
internal bool IsPartOfProjectManifest
{
get { return isPartOfProjectManifest; }
}
/// <summary>
/// Whether this item has backing XML
/// </summary>
internal bool IsBackedByXml
{
get { return xml != null; }
}
/// <summary>
/// Whether the metadata lists have been backed up
/// </summary>
internal bool IsBackedUp
{
get { return unevaluatedCustomMetadataBackup != null; }
}
#endregion
#region Methods
/// <summary>
/// Get the collection of custom metadata. This does not include built-in metadata.
/// </summary>
/// <remarks>
/// RECOMMENDED GUIDELINES FOR METHOD IMPLEMENTATIONS:
/// 1) this method should return a clone of the metadata
/// 2) writing to this dictionary should not be reflected in the underlying item.
/// </remarks>
internal IDictionary CloneCustomMetadata()
{
IDictionary result = (IDictionary)this.evaluatedCustomMetadata.Clone();
return MergeDefaultMetadata(result);
}
/// <summary>
/// Initializes the cache for storing custom attributes (meta-data).
/// </summary>
private void InitializeCustomMetadataCache()
{
this.unevaluatedCustomMetadata = new CopyOnWriteHashtable(StringComparer.OrdinalIgnoreCase);
this.evaluatedCustomMetadata = new CopyOnWriteHashtable(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Initializes a persisted item from an existing item element which exists either in the main project file or in one of
/// the imported files.
/// </summary>
/// <param name="itemElementToParse"></param>
private void InitializeFromItemElement(XmlElement element)
{
this.xml = new BuildItemGroupChildXml(element, ChildType.BuildItemAdd);
this.name = xml.Name;
this.itemSpecModifiers = null;
this.recursivePortionOfFinalItemSpecDirectory = null;
this.evaluatedItemSpecEscaped = xml.Include;
this.finalItemSpecEscaped = xml.Include;
InitializeCustomMetadataCache();
}
/// <summary>
/// Allows the project to save the value of the item's Include attribute after expanding all embedded properties.
/// </summary>
/// <param name="evaluatedItemSpecValueEscaped">Can be null.</param>
internal void SetEvaluatedItemSpecEscaped(string evaluatedItemSpecValueEscaped)
{
this.evaluatedItemSpecEscaped = evaluatedItemSpecValueEscaped;
}
/// <summary>
/// This allows the engine to set the final item spec for this item, after
/// it has been evaluated. A fair question is ... why doesn't the BuildItem
/// object itself have an internal Evaluate() method which allows it
/// to evaluate itself and set its own finalItemSpec. The answer is
/// simply that the BuildItem doesn't have nearly enough information to be
/// able to do this. An BuildItem can only be evaluated in the context of the
/// larger project.
/// </summary>
internal void SetFinalItemSpecEscaped
(
string finalItemSpecValueEscaped
)
{
this.finalItemSpecEscaped = finalItemSpecValueEscaped;
this.itemSpecModifiers = null;
this.recursivePortionOfFinalItemSpecDirectory = null;
}
/// <summary>
/// This method extracts from the final item-spec the portion of its directory that matches the recursive wildcard
/// specification (if any) given in the original item-spec i.e. in the "Include" attribute.
///
/// For the path:
///
/// subdir1\**\debug\*.txt
///
/// Recursive portion:
///
/// bin\debug\
///
/// </summary>
/// <returns>recursively matched portion of item directory</returns>
internal string ExtractRecursivePortionOfFinalItemSpecDirectory()
{
if (recursivePortionOfFinalItemSpecDirectory == null)
{
recursivePortionOfFinalItemSpecDirectory = String.Empty;
// The RecursiveDir may have come from a TaskCreated item.
// In this case, use that instead of deriving it.
if (this.unevaluatedCustomMetadata[FileUtilities.ItemSpecModifiers.RecursiveDir] != null)
{
recursivePortionOfFinalItemSpecDirectory = (string)this.unevaluatedCustomMetadata[FileUtilities.ItemSpecModifiers.RecursiveDir];
}
else
{
// only do this for items whose evaluated Include value is available -- virtual items do not have an Include
// attribute, so it's usually impossible to compute the recursive path (unless the virtual item is a clone of an
// item that has backing XML)
if (evaluatedItemSpecEscaped != null)
{
// Now we are comparing values that are "canonically escaped" by us -- the "FinalItemSpecEscaped", which comes from matching against the file system
// with values that may be escaped somewhat, or not at all -- the "evaluatedItemSpecEscaped", which comes from the literal Include in the XML.
// Anything escaped in that literal Include in the XML should be treated as literal. For example "%28" in the XML should match "(" in the file system.
// To compare like with like, unescape both first.
FileMatcher.Result match = FileMatcher.FileMatch(EscapingUtilities.UnescapeAll(evaluatedItemSpecEscaped), EscapingUtilities.UnescapeAll(FinalItemSpecEscaped));
// even if the evaluated Include value is a legal file spec, it may not match the final item-spec for various
// reasons e.g. Include is a semi-colon separated list, or it is an unsupported //server/share pattern, etc.
if (match.isLegalFileSpec && match.isMatch)
{
recursivePortionOfFinalItemSpecDirectory = match.wildcardDirectoryPart;
}
}
}
}
return recursivePortionOfFinalItemSpecDirectory;
}
/// <summary>
/// Evaluates the item and returns a virtual group containing any resulting items.
/// This allows an item to be evaluated without it belonging to an item group.
/// </summary>
internal BuildItemGroup Evaluate(Expander expander,
string baseDirectory,
bool expandMetadata,
ParserOptions parserOptions,
EngineLoggingServices loggingServices,
BuildEventContext buildEventContext)
{
BuildItemGroup result = new BuildItemGroup();
bool itemCondition = Utilities.EvaluateCondition(Condition,
ConditionAttribute,
expander,
parserOptions,
loggingServices,
buildEventContext);
if (!itemCondition)
{
return result;
}
EvaluateAllItemMetadata(expander, parserOptions, loggingServices, buildEventContext);
BuildItemGroup items = BuildItemGroup.ExpandItemIntoItems(baseDirectory, this, expander, expandMetadata);
for (int i = 0; i < items.Count; i++)
{
BuildItem newItem = CreateClonedParentedItem(items[i], this);
result.AddExistingItem(newItem);
}
return result;
}
/// <summary>
/// Create a clone of the parent item with all the information from the child item.
/// </summary>
/// <remarks>
/// FUTURE: It is unclear what this Whidbey code is for -- the callers already have a child item don't they?
/// Can this be eliminated to avoid excess cloning?
/// </remarks>
internal static BuildItem CreateClonedParentedItem(BuildItem childItem, BuildItem parentItem)
{
BuildItem newItem = parentItem.Clone();
newItem.SetEvaluatedItemSpecEscaped(childItem.EvaluatedItemSpec);
newItem.SetFinalItemSpecEscaped(childItem.FinalItemSpecEscaped);
// If this item was defined based on another item, grab that other
// item's attributes.
newItem.CloneVirtualMetadata();
newItem.ImportVirtualMetadataFrom(childItem);
// BUT if this item itself has attributes declared in the project file,
// then these should clearly win.
newItem.ImportVirtualMetadataFrom(parentItem);
// Set up the parent/child relationship. The "parent" is the actual item
// tag as declared in the project file. The "child" is the item that
// it evaluated to. For example, a parent <Blah Include="*.cs"/>
// may evaluate to many children <Blah Include="a.cs"/>, b.cs, c.cs, etc.
newItem.ParentPersistedItem = parentItem;
return newItem;
}
/// <summary>
/// Populate the lists of evaluated and unevaluated metadata with all metadata that have true conditions.
/// </summary>
/// <remarks>
/// FUTURE: Currently this isn't done when the item is constructed; so for example HasMetadata will always return
/// false until EvaluatedAllItemMetadata is explicitly called. The reason for this is that Metadata are
/// not first class objects, they are merely string pairs produced by running over the child XML with a particular expander.
/// When Metadata are first class objects this method can be changed to merely evaluate them,
/// just as BuildItemGroup.Evaluate does for BuildItem, then methods like HasMetadata behave more sanely. Of course this
/// could be a breaking change.
/// </remarks>
internal void EvaluateAllItemMetadata
(
Expander expander,
ParserOptions parserOptions,
EngineLoggingServices loggingServices,
BuildEventContext buildEventContext
)
{
ErrorUtilities.VerifyThrow(expander != null, "Null expander passed in.");
// Cache all custom attributes on the item. For a persisted item, this will walk the item's child nodes, and
// cache the custom attributes using a "last one wins" policy. For a virtual item, this method does nothing.
// We only evaluate metadata by reading XML
if (IsBackedByXml)
{
ErrorUtilities.VerifyThrow((this.evaluatedCustomMetadata != null) && (this.unevaluatedCustomMetadata != null),
"Item is not initialized properly.");
// We're evaluating from scratch, so clear out any old cached attributes.
this.evaluatedCustomMetadata.Clear();
this.unevaluatedCustomMetadata.Clear();
// Let the expander know about our own item type, so it can
// expand unqualified metadata expressions
SpecificItemDefinitionLibrary specificItemDefinitionLibrary = new SpecificItemDefinitionLibrary(name, itemDefinitionLibrary);
expander = new Expander(expander, specificItemDefinitionLibrary);
List<XmlElement> metadataElements = xml.GetChildren();
// look at all the item's child nodes
foreach (XmlElement metadataElement in metadataElements)
{
// confirm that the child node is not conditionally disabled
bool condition = true;
XmlAttribute conditionAttribute = ProjectXmlUtilities.GetConditionAttribute(metadataElement, true /*no other attributes allowed*/);
if (conditionAttribute != null)
{
condition = Utilities.EvaluateCondition(conditionAttribute.Value,
conditionAttribute, expander, null, parserOptions,
loggingServices, buildEventContext);
}
if (condition)
{
// cache its value, both the evaluated and unevaluated.
string unevaluatedMetadataValue = Utilities.GetXmlNodeInnerContents(metadataElement);
unevaluatedCustomMetadata[metadataElement.Name] = unevaluatedMetadataValue;
string evaluatedMetadataValue = expander.ExpandAllIntoStringLeaveEscaped(unevaluatedMetadataValue, metadataElement);
evaluatedCustomMetadata[metadataElement.Name] = evaluatedMetadataValue;
// Add this metadata to the running table we're using, so that one piece of metadata can refer to another one above
expander.SetMetadataInMetadataTable(name, metadataElement.Name, evaluatedMetadataValue);
}
}
}
}
/// <summary>
/// Indicates if the given metadata is set on the item.
/// </summary>
/// <remarks>BuildItem-spec modifiers are treated as metadata.</remarks>
public bool HasMetadata(string metadataName)
{
ErrorUtilities.VerifyThrowArgumentLength(metadataName, nameof(metadataName));
ErrorUtilities.VerifyThrow(this.unevaluatedCustomMetadata != null, "Item not initialized properly. unevaluatedCustomAttributes is null.");
#if DEBUG
// The hashtable of metadata (this.unevaluatedCustomMetadata) should never contain
// values for those reserved metadata that the engine provides (Filename, RelativeDir,
// Extension, etc.). The one exception is that the hashtable is allowed to contain
// a value for "RecursiveDir" because that one is extra special ... tasks are allowed
// to set that one, because it's the only way for tasks (CreateItem) to provide that value.
// Hence the need to call FileUtilities.IsDerivableItemSpecModifider.
ErrorUtilities.VerifyThrow(!unevaluatedCustomMetadata.ContainsKey(metadataName) || !FileUtilities.IsDerivableItemSpecModifier(metadataName),
"Item-spec modifiers are reserved attributes and cannot be redefined -- this should have been caught when the item was initialized.");
#endif
if (FileUtilities.IsItemSpecModifier(metadataName))
{
return true;
}
if (unevaluatedCustomMetadata.ContainsKey(metadataName))
{
return true;
}
if (GetDefaultMetadataValue(metadataName) != null)
{
return true;
}
return false;
}
/// <summary>
/// Retrieves an arbitrary unevaluated metadata value from the item element. These are pieces of metadata that the project author has
/// placed on the item element that have no meaning to MSBuild. They are just arbitrary metadata that travel around with
/// the BuildItem wherever it goes.
/// </summary>
/// <param name="metadataName">The name of the metadata to retrieve.</param>
/// <returns>The value of the requested metadata.</returns>
/// <exception cref="InvalidOperationException">Thrown when the requested metadata is not applicable to the item.</exception>
public string GetMetadata(string metadataName)
{
string metadataValue;
if (FileUtilities.IsItemSpecModifier(metadataName))
{
// BUGBUG VSWhidbey 377466. If this method is being called directly by an OM
// consumer, then he is in control of the current working directory. This
// means that if he requests the "FullPath" attribute, we will likely compute
// it incorrectly (and furthermore cache the incorrect value for later use
// during the build). What we need to do is make sure to set the current working
// directory to the directory of the project file before attempting to compute
// things like "FullPath" or "RootDir".
metadataValue = GetItemSpecModifier(metadataName);
}
else
{
ErrorUtilities.VerifyThrow(this.unevaluatedCustomMetadata != null, "Item not initialized properly. unevaluatedCustomMetadata is null.");
metadataValue = (string)this.unevaluatedCustomMetadata[metadataName];
}
// If we don't have an explicit value for this metadata then try to find a default value
if (metadataValue == null)
{
metadataValue = GetDefaultMetadataValue(metadataName);
}
return metadataValue ?? String.Empty;
}
/// <summary>
/// Retrieves an arbitrary metadata from the item element, expands any property and item references within it, and
/// unescapes it.
/// </summary>
/// <remarks>Custom attributes on virtual items are not evaluated.</remarks>
/// <param name="metadataName">The name of the attribute to retrieve.</param>
/// <returns>The evaluated value of the requested attribute.</returns>
/// <exception cref="InvalidOperationException">Thrown when the requested attribute is not applicable to the item.</exception>
public string GetEvaluatedMetadata(string metadataName)
{
return EscapingUtilities.UnescapeAll(this.GetEvaluatedMetadataEscaped(metadataName));
}
/// <summary>
/// Retrieves an arbitrary metadata from the item element, expands any property and item references within it.
/// </summary>
/// <remarks>Custom attributes on virtual items are not evaluated.</remarks>
/// <param name="metadataName">The name of the attribute to retrieve.</param>
/// <returns>The evaluated value of the requested attribute.</returns>
/// <exception cref="InvalidOperationException">Thrown when the requested attribute is not applicable to the item.</exception>
internal string GetEvaluatedMetadataEscaped(string metadataName)
{
string metadataValue;
if (FileUtilities.IsItemSpecModifier(metadataName))
{
// BUGBUG VSWhidbey 377466. If this method is being called directly by an OM
// consumer, then he is in control of the current working directory. This
// means that if he requests the "FullPath" attribute, we will likely compute
// it incorrectly (and furthermore cache the incorrect value for later use
// during the build). What we need to do is make sure to set the current working
// directory to the directory of the project file before attempting to compute
// things like "FullPath" or "RootDir".
metadataValue = GetItemSpecModifier(metadataName);
}
else
{
ErrorUtilities.VerifyThrow(this.evaluatedCustomMetadata != null, "Item not initialized properly. evaluatedCustomMetadata is null.");
metadataValue = (string)this.evaluatedCustomMetadata[metadataName];
}
// If we don't have an explicit value for this metadata then try to find a default value
if (metadataValue == null)
{
metadataValue = GetDefaultMetadataValue(metadataName);
}
return metadataValue ?? String.Empty;
}
/// <summary>
/// Retrieves all user-defined/custom attribute names.
/// </summary>
internal List<string> GetAllCustomMetadataNames()
{
ErrorUtilities.VerifyThrow(this.unevaluatedCustomMetadata != null, "Item not initialized properly. unevaluatedCustomMetadata is null.");
List<string> names = new List<string>(GetCustomMetadataCount());
foreach (string name in unevaluatedCustomMetadata.Keys)
{
names.Add(name);
}
if (itemDefinitionLibrary != null)
{
ICollection<string> defaultedNames = itemDefinitionLibrary.GetDefaultedMetadataNames(name);
if (defaultedNames != null)
{
names.AddRange(defaultedNames);
}
}
return names;
}
/// <summary>
/// Indicates how many custom attributes are set on this item.
/// </summary>
/// <remarks>
/// This method does NOT count the pre-defined/reserved item-spec modifiers because they are NOT "custom" attributes.
/// </remarks>
internal int GetCustomMetadataCount()
{
ErrorUtilities.VerifyThrow(this.unevaluatedCustomMetadata != null, "Item not initialized properly. unevaluatedCustomMetadata is null.");
int count = unevaluatedCustomMetadata.Count;
if (itemDefinitionLibrary != null)
{
count += itemDefinitionLibrary.GetDefaultedMetadataCount(name);
}
return count;
}
/// <summary>
/// Copies all custom attributes to given item.
/// </summary>
/// <param name="destinationItem">BuildItem to copy custom attributes to</param>
public void CopyCustomMetadataTo(BuildItem destinationItem)
{
ErrorUtilities.VerifyThrowArgumentNull(destinationItem, nameof(destinationItem));
if (IsBackedByXml)
{
List<XmlElement> children = xml.GetChildren();
foreach (XmlElement child in children)
{
destinationItem.SetMetadata(child.Name, child.InnerXml);
}
}
else
{
ErrorUtilities.VerifyThrow(this.evaluatedCustomMetadata != null, "Item not initialized properly.");
foreach (DictionaryEntry customAttributeEntry in this.evaluatedCustomMetadata)
{
destinationItem.SetMetadata((string)customAttributeEntry.Key, (string)customAttributeEntry.Value);
}
}
}
/// <summary>
/// Clones the hashtables which cache the values of all the custom metadata on this item.
/// Callers should do this when they know that they have a shallow clone of another item,
/// and they want to modify the attributes on this item without touching the original item.
/// </summary>
/// <owner>RGoel</owner>
internal void CloneVirtualMetadata()
{
this.evaluatedCustomMetadata = (CopyOnWriteHashtable)this.evaluatedCustomMetadata.Clone();
this.unevaluatedCustomMetadata = (CopyOnWriteHashtable)this.unevaluatedCustomMetadata.Clone();
}
/// <summary>
/// Without altering the backing XML of this item, copy custom metadata from the given item.
/// </summary>
/// <param name="itemToCopyFrom"></param>
internal void ImportVirtualMetadataFrom(BuildItem itemToCopyFrom)
{
foreach (DictionaryEntry customAttributeEntry in itemToCopyFrom.evaluatedCustomMetadata)
{
this.evaluatedCustomMetadata[customAttributeEntry.Key] = customAttributeEntry.Value;
}
foreach (DictionaryEntry customAttributeEntry in itemToCopyFrom.unevaluatedCustomMetadata)
{
this.unevaluatedCustomMetadata[customAttributeEntry.Key] = customAttributeEntry.Value;
}
Dictionary<string, string> itemDefinitionMetadata = null;
if (!String.Equals(itemToCopyFrom.Name, this.Name, StringComparison.OrdinalIgnoreCase) &&
itemDefinitionLibrary.IsEvaluated)
{
// copy item definition metadata over as "real" metadata
itemDefinitionMetadata = itemDefinitionLibrary.GetDefaultedMetadata(itemToCopyFrom.Name);
}
if (itemDefinitionMetadata != null)
{
foreach (KeyValuePair<string, string> itemDefinitionMetadatum in itemDefinitionMetadata)
{
this.evaluatedCustomMetadata[itemDefinitionMetadatum.Key] = itemDefinitionMetadatum.Value;
}
}
}
/// <summary>
/// Retrieves all user-defined/custom metadata after expanding any property references they might contain.
/// NOTE: Merges them into any default metadata -- so if there are any default metadata, this may return considerably
/// more than just the metadata specifically on this item.
/// </summary>
/// <remarks>
/// This method does NOT return the pre-defined/reserved item-spec modifiers because they are NOT "custom" metadata
/// </remarks>
internal IDictionary GetAllCustomEvaluatedMetadata()
{
ErrorUtilities.VerifyThrow(this.evaluatedCustomMetadata != null, "Item not initialized properly. evaluatedCustomMetadata is null.");
IDictionary result = MergeDefaultMetadata(evaluatedCustomMetadata);
return result;
}
/// <summary>
/// Retrieves all user-defined/custom metadata in unevaluated form.
/// NOTE: Merges them into any default metadata -- so if there are any default metadata, this may return considerably
/// more than just the metadata specifically on this item.
/// </summary>
private IDictionary GetAllCustomUnevaluatedMetadata()
{
ErrorUtilities.VerifyThrow(this.unevaluatedCustomMetadata != null, "Item not initialized properly. unevaluatedCustomMetadata is null.");
IDictionary result = MergeDefaultMetadata(unevaluatedCustomMetadata);
return result;
}
/// <summary>
/// Returns a dictionary containing any default metadata, with the provided set of metadata
/// overlaid on those defaults.
/// </summary>
private IDictionary MergeDefaultMetadata(IDictionary customMetadata)
{
IDictionary result = customMetadata;
if (itemDefinitionLibrary != null)
{
IDictionary defaultedMetadata = itemDefinitionLibrary.GetDefaultedMetadata(name);
if (defaultedMetadata != null)
{
// If we have any defaulted metadata, return a dictionary containing both specific and defaulted
// metadata, with the specific metadata winning if there's a conflict.
result = new CopyOnWriteHashtable(defaultedMetadata, StringComparer.OrdinalIgnoreCase);
foreach (DictionaryEntry metadata in customMetadata)
{
result[metadata.Key] = metadata.Value;
}
}
}
return result;
}
/// <summary>
/// Sets custom metadata on this item, with the option of treating the metadata value
/// literally, meaning that special sharacters will be escaped.
/// Does not backup metadata before making changes.
/// </summary>
public void SetMetadata(string metadataName, string metadataValue, bool treatMetadataValueAsLiteral)
{
SetMetadata(metadataName, treatMetadataValueAsLiteral ? EscapingUtilities.Escape(metadataValue) : metadataValue);
}
private void VerifyForMetadataSet(string metadataName, string metadataValue)
{
ErrorUtilities.VerifyThrowArgument(!FileUtilities.IsDerivableItemSpecModifier(metadataName),
"Shared.CannotChangeItemSpecModifiers", metadataName);
ErrorUtilities.VerifyThrowArgumentLength(metadataName, nameof(metadataName));
ErrorUtilities.VerifyThrowArgumentNull(metadataValue, nameof(metadataValue));
// Make sure the metadata doesn't use any special characters in the name.
XmlUtilities.VerifyThrowValidElementName(metadataName);
ErrorUtilities.VerifyThrowInvalidOperation(XMakeElements.IllegalItemPropertyNames[metadataName] == null,
"CannotModifyReservedItemMetadata", metadataName);
ErrorUtilities.VerifyThrow((this.unevaluatedCustomMetadata != null) && (this.evaluatedCustomMetadata != null),
"Item not initialized properly.");
}
/// <summary>
/// Updates only the metadata tables, not the backing XML. Keep a backup so that the changes
/// can be reverted.
/// </summary>
internal void SetVirtualMetadata(string metadataName, string metadataValue)
{
VerifyForMetadataSet(metadataName, metadataValue);
if (IsPartOfProjectManifest)
{
BackupPersistedMetadata();
}
unevaluatedCustomMetadata[metadataName] = metadataValue;
evaluatedCustomMetadata[metadataName] = metadataValue;
}
private void BackupPersistedMetadata()
{
if (!IsBackedUp)
{
unevaluatedCustomMetadataBackup = (CopyOnWriteHashtable)unevaluatedCustomMetadata.Clone();
evaluatedCustomMetadataBackup = (CopyOnWriteHashtable)evaluatedCustomMetadata.Clone();
}
}
/// <summary>
/// If the metadata tables were backed up, revert them to the originals and
/// throw out the backups.
/// </summary>
internal void RevertToPersistedMetadata()
{
if (IsBackedUp)
{
unevaluatedCustomMetadata = unevaluatedCustomMetadataBackup;
evaluatedCustomMetadata = evaluatedCustomMetadataBackup;
unevaluatedCustomMetadataBackup = null;
evaluatedCustomMetadataBackup = null;
}
}
/// <summary>
/// Sets an arbitrary metadata on the item element. These are metadata that the project author has placed on the item
/// element that have no meaning to MSBuild. They are just arbitrary metadata that travel around with the BuildItem.
/// Does not backup metadata before making changes.
/// </summary>
public void SetMetadata(string metadataName, string metadataValue)
{
MustNotBeImported();
VerifyForMetadataSet(metadataName, metadataValue);
// this will let us make sure the project item has actually changed before marking it dirty
bool dirtyItem = false;
if (IsPartOfProjectManifest)
{
SplitChildItemIfNecessary();
dirtyItem = xml.SetChildValue(metadataName, metadataValue);
}
// update the custom metadata cache
this.unevaluatedCustomMetadata[metadataName] = metadataValue;
this.evaluatedCustomMetadata[metadataName] = metadataValue;
if (dirtyItem && IsPartOfProjectManifest)
{
// mark the item as dirty, one of the pieces of metadata (or the overall list) has changed;
// this will mark the parent project as dirty
MarkItemAsDirty();
}
}
/// <summary>
/// Removes the specified metadata on the item.
/// </summary>
/// <remarks>Removal of well-known metadata is not allowed.</remarks>
public void RemoveMetadata(string metadataName)
{
ErrorUtilities.VerifyThrowArgument(!FileUtilities.IsItemSpecModifier(metadataName), "Shared.CannotChangeItemSpecModifiers", metadataName);
MustNotBeImported();
ErrorUtilities.VerifyThrow((this.unevaluatedCustomMetadata != null) && (this.evaluatedCustomMetadata != null), "Item not initialized properly.");
if (IsBackedByXml)
{
SplitChildItemIfNecessary();
xml.RemoveChildrenByName(metadataName);
}
// update the custom attribute cache
this.unevaluatedCustomMetadata.Remove(metadataName);
this.evaluatedCustomMetadata.Remove(metadataName);
MarkItemAsDirty();
}
/// <summary>
/// Returns any default value that this item knows about for the named metadata.
/// If none is known, returns null.
/// </summary>
private string GetDefaultMetadataValue(string metadataName)
{
string metadataValue = null;
if (name != null && itemDefinitionLibrary != null)
{
metadataValue = itemDefinitionLibrary.GetDefaultMetadataValue(name, metadataName);
}
return metadataValue;
}
/// <summary>
/// Performs path manipulations on the (evaluated/final) item-spec as directed.
/// </summary>
/// <param name="modifier">The modifier to apply to the item-spec, such as RootDir or Filename.</param>
/// <returns>The modified item-spec.</returns>
/// <exception cref="InvalidOperationException">Thrown when the item-spec is not a path.</exception>
private string GetItemSpecModifier(string modifier)
{
string modifiedItemSpec =
FileUtilities.GetItemSpecModifier(Project.PerThreadProjectDirectory, FinalItemSpecEscaped, modifier, ref itemSpecModifiers);
if (modifiedItemSpec.Length == 0)
{
if (String.Equals(modifier, FileUtilities.ItemSpecModifiers.RecursiveDir, StringComparison.OrdinalIgnoreCase))
{
modifiedItemSpec = ExtractRecursivePortionOfFinalItemSpecDirectory();
ErrorUtilities.VerifyThrow(itemSpecModifiers != null,
"The FileUtilities.GetItemSpecModifier() method should have created the cache for the \"{0}\" modifier.",
FileUtilities.ItemSpecModifiers.RecursiveDir);
itemSpecModifiers[modifier] = modifiedItemSpec;
}
}
return modifiedItemSpec;
}
/// <summary>
/// Determines if the new item spec that the user is trying to add to the project
/// already matches an existing wildcarded item declared in the project. We only
/// consider it a "match" in very specific circumstances... if there's anything
/// weird or not-mainline about the new item spec or the existing item, we fail
/// the match in order to "play it safe".
/// </summary>
internal bool NewItemSpecMatchesExistingWildcard(string newItemSpec)
{
Project parentProject = GetParentProject();
ErrorUtilities.VerifyThrow(parentProject != null, "This method should only get called on persisted items.");
BuildPropertyGroup evaluatedProperties = parentProject.evaluatedProperties;
if (
// The original item spec should have had at least one "*" or "?" in it.
FileMatcher.HasWildcards(this.Include) &&
// The original item should not have a Condition.
(this.Condition.Length == 0) &&
// The original item should not have an Exclude.
(this.Exclude.Length == 0) &&
// The new item spec should NOT have any wildcards.
!FileMatcher.HasWildcards(newItemSpec)
)
{
Expander propertyExpander = new Expander(evaluatedProperties);
string newItemSpecExpandedEscaped = propertyExpander.ExpandAllIntoStringLeaveEscaped(newItemSpec, null);
// New item spec should not have any unescaped semicolons ... this can really mess us up.
if (-1 == newItemSpecExpandedEscaped.IndexOf(';'))
{
// Expand any properties in the new item spec that the user gave us.
string newItemSpecExpandedUnescaped = EscapingUtilities.UnescapeAll(newItemSpecExpandedEscaped);
// Loop over each piece separated by semicolons.
List<string> itemIncludePieces = propertyExpander.ExpandAllIntoStringListLeaveEscaped(this.Include, this.IncludeAttribute);
foreach (string itemIncludePiece in itemIncludePieces)
{
bool containsEscapedWildcards = EscapingUtilities.ContainsEscapedWildcards(itemIncludePiece);
bool containsRealWildcards = FileMatcher.HasWildcards(itemIncludePiece);
// If this is the piece that has the wildcards ...
if (containsRealWildcards && !containsEscapedWildcards)
{
string itemIncludePieceContainingWildcardUnescaped = EscapingUtilities.UnescapeAll(itemIncludePiece);
FileMatcher.Result match = FileMatcher.FileMatch(itemIncludePieceContainingWildcardUnescaped, newItemSpecExpandedUnescaped);
if (match.isLegalFileSpec && match.isMatch)
{
// The wildcard in the original item spec will match the new item that
// user is trying to add.
return true;
}
}
}
}
}
return false;
}
/// <summary>
/// Gets the parent project of this item, if there is one.
/// </summary>
private Project GetParentProject()
{
BuildItemGroup parentItemGroup;
// If this is a child item, then it's really the parent persisted item (the thing that's
// actually in the project file) that has a pointer to the parent BuildItemGroup.
if (this.ParentPersistedItem != null)
{
parentItemGroup = this.ParentPersistedItem.ParentPersistedItemGroup;
ErrorUtilities.VerifyThrow(parentItemGroup != null, "Inconsistency -- is this item persisted or not?");
}
else
{
parentItemGroup = this.ParentPersistedItemGroup;
}
if (parentItemGroup != null)
{
ErrorUtilities.VerifyThrowNoAssert(parentItemGroup.ParentProject != null, "Persisted BuildItemGroup doesn't have parent project.");
return parentItemGroup.ParentProject;
}
else
{
return null;
}
}
/// <summary>
/// Marks the parent project as dirty.
/// </summary>
private void MarkItemAsDirty()
{
Project parentProject = GetParentProject();
parentProject?.MarkProjectAsDirty();
}
/// <summary>
/// Marks the parent project as dirty for re-evaluation only. This means that the actual contents
/// of the project file XML haven't changed.
/// </summary>
private void MarkItemAsDirtyForReevaluation()
{
Project parentProject = GetParentProject();
parentProject?.MarkProjectAsDirtyForReevaluation();
}
/// <summary>
/// Verifies this is not an imported item.
/// </summary>
private void MustNotBeImported()
{
ErrorUtilities.VerifyThrowInvalidOperation(!importedFromAnotherProject, "CannotModifyImportedProjects");
}
/// <summary>
/// Verifies that an item definition library is supplied.
/// This is for internal error checking only.
/// </summary>
private void MustHaveItemDefinitionLibrary(ItemDefinitionLibrary library)
{
ErrorUtilities.VerifyThrow(library != null, "Item definition library required when item is owned by project");
}
/// <summary>
/// Sometimes an item in the project file is declared such that it actually evaluates
/// to multiple items (we call these "child" items). Examples include the use of
/// wildcards, as well as semicolon-separated lists of item specs. When one of the
/// child items is modified through the object model, in order to reflect that
/// change in the project file, we have to split up the original item tag into
/// multiple item tags. This method, when called on one of the parent items,
/// accomplishes exactly that.
/// </summary>
private void SplitItem()
{
if (!IsPartOfProjectManifest)
{
// There's no point splitting an item if it's not an item outside of a target;
// a modification to such an item is thrown away at the end of the build, so we'll
// never persist it
return;
}
ErrorUtilities.VerifyThrow(ParentPersistedItemGroup != null, "No parent BuildItemGroup for item to be removed.");
if (this.ChildItems.Count > 1)
{
foreach (BuildItem childItem in this.ChildItems)
{
ErrorUtilities.VerifyThrow(childItem.IsBackedByXml, "Must be backed by XML");
childItem.ParentPersistedItem = null;
BuildItem newItem = ParentPersistedItemGroup.AddNewItem(childItem.Name, childItem.FinalItemSpecEscaped);
if (childItem.Condition.Length > 0)
{
newItem.Condition = childItem.Condition;
}
// Copy all the metadata from the original item tag.
foreach (XmlNode customMetadata in childItem.ItemElement)
{
newItem.ItemElement.AppendChild(customMetadata.Clone());
}
childItem.InitializeFromItemElement(newItem.ItemElement);
childItem.ParentPersistedItem = newItem;
newItem.ChildItems.AddItem(childItem);
}
ParentPersistedItemGroup.RemoveItem(this);
}
}
/// <summary>
/// Sometimes an item in the project file is declared such that it actually evaluates
/// to multiple items (we call these "child" items). Examples include the use of
/// wildcards, as well as semicolon-separated lists of item specs. When one of the
/// child items is modified through the object model, in order to reflect that
/// change in the project file, we have to split up the original item tag into
/// multiple item tags. This method, when called on one of the child items,
/// accomplishes exactly that.
/// </summary>
/// <owner>rgoel</owner>
internal void SplitChildItemIfNecessary()
{
this.ParentPersistedItem?.SplitItem();
}
/// <summary>
/// This creates a shallow clone of the BuildItem. If this is an xml-backed item,
/// then the clone references the same XML element as the original, meaning
/// that modifications to the clone will affect the original.
/// </summary>
/// <returns></returns>
/// <owner>rgoel</owner>
public BuildItem Clone()
{
BuildItem clonedItem;
if (IsBackedByXml)
{
clonedItem = new BuildItem(xml.Element, this.importedFromAnotherProject, this.itemDefinitionLibrary);
clonedItem.SetEvaluatedItemSpecEscaped(this.evaluatedItemSpecEscaped);
clonedItem.SetFinalItemSpecEscaped(this.FinalItemSpecEscaped);
clonedItem.itemSpecModifiers = this.itemSpecModifiers;
clonedItem.recursivePortionOfFinalItemSpecDirectory = this.recursivePortionOfFinalItemSpecDirectory;
clonedItem.evaluatedCustomMetadata = this.evaluatedCustomMetadata;
clonedItem.unevaluatedCustomMetadata = this.unevaluatedCustomMetadata;
clonedItem.isPartOfProjectManifest = this.isPartOfProjectManifest;
clonedItem.itemDefinitionLibrary = this.itemDefinitionLibrary;
}
else
{
clonedItem = VirtualClone();
}
// Do not set the ParentPersistedItemGroup on the cloned item, because it isn't really
// part of the item group.
return clonedItem;
}
/// <summary>
/// Updates the build item xml backing store with the passed in xml backing store.
/// </summary>
internal void UpdateBackingXml(BuildItemGroupChildXml backingXml)
{
xml = backingXml;
this.name = xml.Name;
}
internal BuildItem VirtualClone()
{
return VirtualClone(false /* do not remove references */);
}
/// <summary>
/// Clones the item to a virtual item i.e. an item with no backing XML.
/// If removeReferences is specified, removes all references which hold on to Projects (or other heavyweight objects)
/// in order to minimize the transitive size of the clone.
/// </summary>
/// <remarks>
/// This method differs from Clone() in that it always produces a virtual item, even when given an item with backing XML.
/// Decoupling an item from its XML allows modifications to the clone without affecting the original item.
/// </remarks>
internal BuildItem VirtualClone(bool removeReferences)
{
ItemDefinitionLibrary definitionLibraryToClone = this.itemDefinitionLibrary;
if (removeReferences)
{
definitionLibraryToClone = null;
}
BuildItem virtualClone =
new BuildItem
(
null /* this is a virtual item with no backing XML */,
name, Include,
false, /* PERF NOTE: don't waste time creating a new custom metadata
* cache, because we're going to clone the current item's cache */
definitionLibraryToClone
);
virtualClone.SetEvaluatedItemSpecEscaped(evaluatedItemSpecEscaped);
virtualClone.SetFinalItemSpecEscaped(FinalItemSpecEscaped);
// NOTE: itemSpecModifiers don't need to be deep-cloned because they are tied to the finalItemSpec -- when the
// finalItemSpec changes, they will get reset
virtualClone.itemSpecModifiers = itemSpecModifiers;
virtualClone.recursivePortionOfFinalItemSpecDirectory = recursivePortionOfFinalItemSpecDirectory;
ErrorUtilities.VerifyThrow(unevaluatedCustomMetadata != null && evaluatedCustomMetadata != null, "Item is not initialized properly.");
if (removeReferences)
{
// The ItemDefinition is going to be cleared to remove a link between a project instance and the Item when it is in the cache of targetOutputs.
// We need to merge the ItemDefinition metadata onto the builditemto preserve the metadata on the buildItem when the ItemDefinition
// is cleared and the item is placed in the cache.
virtualClone.unevaluatedCustomMetadata = (CopyOnWriteHashtable)GetAllCustomUnevaluatedMetadata();
virtualClone.evaluatedCustomMetadata = (CopyOnWriteHashtable)GetAllCustomEvaluatedMetadata();
}
else
{
// Cloning is cheap for CopyOnWriteHashtable so just always do it.
virtualClone.unevaluatedCustomMetadata = (CopyOnWriteHashtable)this.unevaluatedCustomMetadata.Clone();
virtualClone.evaluatedCustomMetadata = (CopyOnWriteHashtable)this.evaluatedCustomMetadata.Clone();
}
return virtualClone;
}
/// <summary>
/// Convert the given array of BuildItems into ITaskItems (BuildItem names are lost)
/// </summary>
/// <param name="originalItems"></param>
/// <returns></returns>
internal static ITaskItem[] ConvertBuildItemArrayToTaskItems(BuildItem[] originalItems)
{
if (originalItems == null)
{
return null;
}
ITaskItem[] convertedItems = new TaskItem[originalItems.Length];
for (int i = 0; i < originalItems.Length; i++)
{
if (originalItems[i].IsUninitializedItem)
{
convertedItems[i] = new TaskItem(originalItems[i].FinalItemSpecEscaped);
foreach (DictionaryEntry metadata in originalItems[i].GetAllCustomEvaluatedMetadata())
{
convertedItems[i].SetMetadata((string)metadata.Key, EscapingUtilities.UnescapeAll((string)metadata.Value));
}
}
else
{
convertedItems[i] = new TaskItem(originalItems[i]);
}
}
return convertedItems;
}
/// <summary>
/// Convert the given array of ITaskItems into nameless BuildItems
/// </summary>
/// <param name="originalItems"></param>
/// <returns></returns>
internal static BuildItem[] ConvertTaskItemArrayToBuildItems(ITaskItem[] originalItems)
{
if (originalItems == null)
{
return null;
}
BuildItem[] convertedItems = new BuildItem[originalItems.Length];
for (int i = 0; i < originalItems.Length; i++)
{
convertedItems[i] = new BuildItem(null, originalItems[i]);
}
return convertedItems;
}
#endregion
}
}
|