|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Build.Collections;
using Microsoft.Build.Construction;
using Microsoft.Build.Framework;
using Microsoft.Build.ObjectModelRemoting;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
#nullable disable
namespace Microsoft.Build.Evaluation
{
/// <summary>
/// An evaluated design-time item
/// </summary>
/// <remarks>
/// Edits to this object will indirectly dirty the containing project because they will modify the backing XML.
/// </remarks>
/// <comment>
/// We cannot use a copy-on-write table for the metadata, as ProjectMetadata objects are mutable. However,
/// we do use it for build-time items.
/// </comment>
[DebuggerDisplay("{ItemType}={EvaluatedInclude} [{UnevaluatedInclude}] #DirectMetadata={DirectMetadataCount}")]
public class ProjectItem : IItem<ProjectMetadata>, IProjectMetadataParent, IItemData
{
/// <summary>
/// Project that this item lives in.
/// ProjectItems always live in a project.
/// Used to get item definitions and project directory.
/// </summary>
private readonly Project _project;
/// <summary>
/// Fragment of the original include that led to this item,
/// with properties expanded but not wildcards. Escaped as necessary
/// </summary>
/// <remarks>
/// This is ONLY used to figure out %(RecursiveDir) when it is requested.
/// It's likely too expensive to figure that out if it isn't needed, so we store
/// the necessary material here.
/// </remarks>
private readonly string _evaluatedIncludeBeforeWildcardExpansionEscaped;
/// <summary>
/// Item definitions are stored in one single table shared by all items of a particular item type.
///
/// When an item is created from another item, such as by using an expression like Include="@(x)",
/// any item definition metadata those source items have must override any item definition metadata
/// associated with the new item type.
///
/// Copying all those item definition metadata into real metadata on this item would be very inefficient, because
/// it would turn a single shared table into a separate table for every item.
///
/// Instead, we get a reference to the item definition of the source items, and consult
/// that table before we consult our own item type's item definition. Since item definitions can't change at this point,
/// it's safe to reference their original table.
///
/// If our item gets copied again, we need a reference to the inherited item definition and we need the real item
/// definition of the source items. Thus a list is created. On copying, a list is created, beginning with a clone
/// of any list the source item had, and ending with the item definition list of the source item type.
///
/// When we look up a metadata value we look at
/// (1) directly associated metadata and built-in metadata
/// (2) the inherited item definition list, starting from the top
/// (3) the item definition associated with our item type
/// </summary>
private readonly List<ProjectItemDefinition> _inheritedItemDefinitions;
/// <summary>
/// Backing XML item.
/// Can never be null
/// </summary>
private ProjectItemElement _xml;
/// <summary>
/// Evaluated include.
/// The original XML may have evaluated to several of these items,
/// each with a different include.
/// May be empty, for example from expanding an empty list or from a transform with undefined metadata.
/// Escaped as necessary
/// </summary>
private string _evaluatedIncludeEscaped;
/// <summary>
/// Collection of metadata that link the XML metadata and evaluated metadata.
/// Since evaluation has occurred, this is an unordered collection.
/// May be null.
/// </summary>
/// <remarks>
/// Lazily created, as there are lots of items
/// that have no metadata at all.
/// </remarks>
private PropertyDictionary<ProjectMetadata> _directMetadata;
/// <summary>
/// Cached value of the fullpath metadata. All other metadata are computed on demand.
/// </summary>
private string _fullPath;
/// <summary>
/// External projects support
/// </summary>
internal ProjectItem(ProjectItemElement xml, Project project)
{
this._project = project;
this._xml = xml;
}
/// <summary>
/// Called by the Evaluator during project evaluation.
/// Direct metadata may be null, indicating no metadata. It is assumed to have already been cloned.
/// Inherited item definition metadata may be null. It is assumed that its list has already been cloned.
/// ProjectMetadata objects may be shared with other items.
/// </summary>
internal ProjectItem(
Project project,
ProjectItemElement xml,
string evaluatedIncludeEscaped,
string evaluatedIncludeBeforeWildcardExpansionEscaped,
PropertyDictionary<ProjectMetadata> directMetadataCloned,
List<ProjectItemDefinition> inheritedItemDefinitionsCloned)
{
ErrorUtilities.VerifyThrowInternalNull(project);
ErrorUtilities.VerifyThrowArgumentNull(xml);
// Orcas accidentally allowed empty includes if they resulted from expansion: we preserve that bug
ErrorUtilities.VerifyThrowArgumentNull(evaluatedIncludeEscaped);
ErrorUtilities.VerifyThrowArgumentNull(evaluatedIncludeBeforeWildcardExpansionEscaped);
_xml = xml;
_project = project;
_evaluatedIncludeEscaped = evaluatedIncludeEscaped;
_evaluatedIncludeBeforeWildcardExpansionEscaped = evaluatedIncludeBeforeWildcardExpansionEscaped;
_directMetadata = directMetadataCloned;
_inheritedItemDefinitions = inheritedItemDefinitionsCloned;
}
internal virtual ProjectItemLink Link => null;
/// <inheritdoc cref="IItemData.EnumerateMetadata"/>
IEnumerable<KeyValuePair<string, string>> IItemData.EnumerateMetadata() => Metadata.Select(m => new KeyValuePair<string, string>(m.Name, m.EvaluatedValue));
/// <summary>
/// Backing XML item.
/// Can never be null.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public ProjectItemElement Xml
{
[DebuggerStepThrough]
get
{ return _xml; }
}
/// <summary>
/// Gets or sets the type of this item.
/// </summary>
public string ItemType
{
[DebuggerStepThrough]
get
{ return _xml.ItemType; }
set
{
if (Link != null)
{
Link.ChangeItemType(value);
}
else
{
ChangeItemType(value);
}
}
}
/// <summary>
/// Gets or sets the unevaluated value of the Include.
/// </summary>
public string UnevaluatedInclude
{
[DebuggerStepThrough]
get
{
return _xml.Include;
}
set
{
Rename(value);
}
}
/// <inheritdoc cref="IItemData.EvaluatedInclude"/>
/// <remarks>
/// Gets the evaluated value of the include, unescaped.
/// </remarks>
public string EvaluatedInclude
{
[DebuggerStepThrough]
get
{ return Link != null ? Link.EvaluatedInclude : EscapingUtilities.UnescapeAll(_evaluatedIncludeEscaped); }
}
/// <summary>
/// Gets the evaluated value of the include, escaped as necessary.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
string IItem.EvaluatedIncludeEscaped
{
[DebuggerStepThrough]
get
{ return _evaluatedIncludeEscaped; }
}
/// <summary>
/// The directory of the project being built
/// Never null: If there is no project filename yet, it will use the current directory
/// </summary>
string IItem.ProjectDirectory
{
get { return _project.DirectoryPath; }
}
/// <summary>
/// Project that this item lives in.
/// ProjectItems always live in a project.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public Project Project
{
[DebuggerStepThrough]
get
{ return _project; }
}
/// <summary>
/// If the item originated in an imported file, returns true.
/// Otherwise returns false.
/// </summary>
public bool IsImported
{
get
{
bool isImported = !Object.ReferenceEquals(_xml.ContainingProject, _project.Xml);
return isImported;
}
}
/// <summary>
/// Metadata directly on the item, if any.
/// Does not include metadata from item definitions.
/// Does not include built-in metadata.
/// Never returns null.
/// </summary>
public IEnumerable<ProjectMetadata> DirectMetadata
{
get { return Link != null ? Link.DirectMetadata : (IEnumerable<ProjectMetadata>)_directMetadata ?? (IEnumerable<ProjectMetadata>)ReadOnlyEmptyCollection<ProjectMetadata>.Instance; }
}
/// <summary>
/// Count of direct metadata on this item, if any.
/// Does NOT count any metadata inherited from item definitions.
/// Does not count built-in metadata, such as "FullPath".
/// </summary>
public int DirectMetadataCount
{
[DebuggerStepThrough]
get
{ return Link != null ? Link.DirectMetadata.Count : _directMetadata != null ? _directMetadata.Count : 0; }
}
/// <summary>
/// Metadata on the item, if any. Includes metadata specified by the definition, if any.
/// If there is no metadata, returns an empty collection.
/// Does not include built-in metadata, such as "FullPath".
/// Get the values of built-in metadata using <see cref="GetMetadataValue(string)"/>.
/// This is a read-only collection. To modify the metadata, use <see cref="SetMetadataValue(string, string)"/>.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")]
public ICollection<ProjectMetadata> Metadata
{
[DebuggerStepThrough]
get
{ return Link != null ? Link.MetadataCollection : MetadataCollection; }
}
IEnumerable<ProjectMetadata> IItem<ProjectMetadata>.Metadata => Metadata;
/// <summary>
/// Count of metadata on this item, if any.
/// Includes any metadata inherited from item definitions.
/// Includes both custom and built-in metadata.
/// </summary>
public int MetadataCount
{
[DebuggerStepThrough]
get
{ return Metadata.Count + FileUtilities.ItemSpecModifiers.All.Length; }
}
/// <summary>
/// Implementation of IKeyed exposing the item type, so items
/// can be put in a dictionary conveniently.
/// </summary>
string IKeyed.Key
{
[DebuggerStepThrough]
get
{ return ItemType; }
}
/// <summary>
/// Internal version of <see cref="Metadata">Metadata</see> that returns
/// a full ICollection.
/// Unordered collection of evaluated metadata on the item.
/// If there is no metadata, returns an empty collection.
/// Does not include built-in metadata.
/// Includes any from item definitions not masked by directly set metadata.
/// This is a read-only collection. To modify the metadata, use <see cref="SetMetadataValue(string, string)"/>.
/// </summary>
internal ICollection<ProjectMetadata> MetadataCollection
{
get
{
RetrievableEntryHashSet<ProjectMetadata> allMetadata = new RetrievableEntryHashSet<ProjectMetadata>(MSBuildNameIgnoreCaseComparer.Default);
// Lowest priority: regular item definitions
ProjectItemDefinition itemDefinition;
if (_project.ItemDefinitions.TryGetValue(ItemType, out itemDefinition))
{
foreach (ProjectMetadata metadataFromDefinition in itemDefinition.Metadata)
{
allMetadata[metadataFromDefinition.Name] = metadataFromDefinition;
}
}
// Next, any inherited item definitions. Front of the list is highest priority,
// so walk backwards.
if (_inheritedItemDefinitions != null)
{
for (int i = _inheritedItemDefinitions.Count - 1; i >= 0; i--)
{
foreach (ProjectMetadata metadatum in _inheritedItemDefinitions[i].Metadata)
{
allMetadata[metadatum.Name] = metadatum;
}
}
}
// Finally any direct metadata win.
if (_directMetadata != null)
{
foreach (ProjectMetadata metadatum in _directMetadata)
{
allMetadata[metadatum.Name] = metadatum;
}
}
return allMetadata.Values;
}
}
/// <summary>
/// Accesses the unescaped evaluated include prior to wildcard expansion
/// </summary>
internal string EvaluatedIncludeBeforeWildcardExpansion
{
[DebuggerStepThrough]
get
{ return EscapingUtilities.UnescapeAll(_evaluatedIncludeBeforeWildcardExpansionEscaped); }
}
/// <summary>
/// Accesses the evaluated include prior to wildcard expansion
/// </summary>
internal string EvaluatedIncludeBeforeWildcardExpansionEscaped
{
[DebuggerStepThrough]
get
{ return _evaluatedIncludeBeforeWildcardExpansionEscaped; }
}
/// <summary>
/// Accesses the inherited item definitions, if any.
/// Used ONLY by the ProjectInstance, when cloning a ProjectItem.
/// </summary>
internal List<ProjectItemDefinition> InheritedItemDefinitions
{
[DebuggerStepThrough]
get
{ return _inheritedItemDefinitions; }
}
/// <summary>
/// Gets an evaluated metadata on this item.
/// Potentially includes a metadata from an item definition.
/// Does not return built-in metadata, such as "FullPath".
/// Returns null if not found.
/// </summary>
public ProjectMetadata GetMetadata(string name)
{
if (Link != null)
{
return Link.GetMetadata(name);
}
ErrorUtilities.VerifyThrowArgumentLength(name);
ProjectMetadata result = null;
if (_directMetadata != null)
{
result = _directMetadata[name];
}
if (result == null)
{
result = GetItemDefinitionMetadata(name);
}
return result;
}
/// <summary>
/// Get the evaluated value of a metadata on this item, possibly from an item definition.
/// Returns empty string if it does not exist.
/// To determine whether a piece of metadata does not exist vs. simply has no value, use <see cref="HasMetadata(string)">HasMetadata</see>.
/// May be used to access the value of built-in metadata, such as "FullPath".
/// Attempting to get built-in metadata on a value that is not a valid path throws InvalidOperationException.
/// </summary>
public string GetMetadataValue(string name)
{
return Link != null ? Link.GetMetadataValue(name) : EscapingUtilities.UnescapeAll(((IItem)this).GetMetadataValueEscaped(name));
}
/// <summary>
/// Returns true if a particular piece of metadata is defined on this item,
/// otherwise false.
/// Includes built-in metadata and metadata inherited from item definitions.
/// </summary>
public bool HasMetadata(string name)
{
if (Link != null)
{
return Link.HasMetadata(name);
}
if (_directMetadata?.Contains(name) == true)
{
return true;
}
if (FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name))
{
return true;
}
ProjectMetadata metadatum = GetItemDefinitionMetadata(name);
if (metadatum != null)
{
return true;
}
return false;
}
/// <summary>
/// See <see cref="GetMetadataValue(string)">GetMetadataValue</see> for a more detailed explanation.
/// Returns the escaped value of the metadatum requested.
/// </summary>
string IItem.GetMetadataValueEscaped(string name)
{
ErrorUtilities.VerifyThrowArgumentLength(name);
string value = null;
if (_directMetadata != null)
{
ProjectMetadata metadatum = _directMetadata[name];
if (metadatum != null)
{
value = metadatum.EvaluatedValueEscaped;
}
}
if (value == null)
{
value = GetBuiltInMetadataEscaped(name);
}
if (value == null)
{
ProjectMetadata metadatum = GetItemDefinitionMetadata(name);
if (metadatum != null && Expander<ProjectProperty, ProjectItem>.ExpressionMayContainExpandableExpressions(metadatum.EvaluatedValueEscaped))
{
Expander<ProjectProperty, ProjectItem> expander = new Expander<ProjectProperty, ProjectItem>(null, null, new BuiltInMetadataTable(this), FileSystems.Default);
value = expander.ExpandIntoStringLeaveEscaped(metadatum.EvaluatedValueEscaped, ExpanderOptions.ExpandBuiltInMetadata, metadatum.Location);
}
else if (metadatum != null)
{
return metadatum.EvaluatedValueEscaped;
}
}
return value ?? String.Empty;
}
/// <summary>
/// Gets any existing ProjectMetadata on the item, or
/// else any on an applicable item definition.
/// This is ONLY called during evaluation.
/// Does not return built-in metadata, such as "FullPath".
/// Returns null if not found.
/// </summary>
ProjectMetadata IItem<ProjectMetadata>.GetMetadata(string name)
{
return GetMetadata(name);
}
/// <summary>
/// Adds a ProjectMetadata to the item.
/// This is ONLY called during evaluation and does not affect the XML.
/// </summary>
ProjectMetadata IItem<ProjectMetadata>.SetMetadata(ProjectMetadataElement metadataElement, string evaluatedInclude)
{
_directMetadata ??= new PropertyDictionary<ProjectMetadata>();
ProjectMetadata predecessor = GetMetadata(metadataElement.Name);
ProjectMetadata metadatum = new ProjectMetadata(this, metadataElement, evaluatedInclude, predecessor);
_directMetadata.Set(metadatum);
return metadatum;
}
/// <summary>
/// Adds metadata with the specified name and value to the item.
/// Updates an existing metadata if one already exists with the same name on the item directly, as opposed to inherited from an item definition.
/// Updates the evaluated project, but does not affect anything else in the project until reevaluation. For example,
/// if a piece of metadata named "m" is added on item of type "i", it does not affect "j" which is evaluated from "@(j->'%(m)')" until reevaluation.
/// Also if the unevaluated value of "m" is set to something that is modified by evaluation, such as "$(p)", the evaluated value will be set to literally "$(p)" until reevaluation.
/// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state without a reevaluation.
/// Returns the new or existing metadatum.
/// </summary>
/// <remarks>Unevaluated value is assumed to be escaped as necessary</remarks>
public ProjectMetadata SetMetadataValue(string name, string unevaluatedValue)
{
return Link != null ? Link.SetMetadataValue(name, unevaluatedValue, false) :
SetMetadataOperation(name, unevaluatedValue, propagateMetadataToSiblingItems: false);
}
/// <summary>
/// Overload of <see cref="SetMetadataValue(string,string)"/>. Adds the option of not splitting the item element and thus affecting all sibling items.
/// Sibling items are defined as all ProjectItem instances that were created from the same item element.
///
/// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state without a reevaluation
/// </summary>
/// /// <param name="name">Metadata name</param>
/// <param name="unevaluatedValue">Metadata value</param>
/// <param name="propagateMetadataToSiblingItems">
/// If true, adds direct metadata to the <see cref="ProjectItemElement"/> from which this <see cref="ProjectItem"/> originated. The intent is to affect all other sibling items.
/// </param>
/// <returns>Returns the new or existing metadatum.</returns>
public ProjectMetadata SetMetadataValue(string name, string unevaluatedValue, bool propagateMetadataToSiblingItems)
{
return Link != null ? Link.SetMetadataValue(name, unevaluatedValue, propagateMetadataToSiblingItems) :
SetMetadataOperation(name, unevaluatedValue, propagateMetadataToSiblingItems: propagateMetadataToSiblingItems);
}
private ProjectMetadata SetMetadataOperation(string name, string unevaluatedValue, bool propagateMetadataToSiblingItems)
{
Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject);
XmlUtilities.VerifyThrowArgumentValidElementName(name);
ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name);
ErrorUtilities.VerifyThrowInvalidOperation(!XMakeElements.ReservedItemNames.Contains(name), "CannotModifyReservedItemMetadata", name);
ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent?.Parent != null, "OM_ObjectIsNoLongerActive");
if (!propagateMetadataToSiblingItems)
{
_project.SplitItemElementIfNecessary(_xml);
}
ProjectMetadata metadatum;
if (_directMetadata?.Contains(name) == true)
{
metadatum = _directMetadata[name];
metadatum.UnevaluatedValue = unevaluatedValue;
}
else
{
ProjectMetadataElement metadatumXml = _xml.AddMetadata(name, unevaluatedValue);
string evaluatedValueEscaped = _project.ExpandMetadataValueBestEffortLeaveEscaped(this, unevaluatedValue, metadatumXml.Location);
metadatum = new ProjectMetadata(this, metadatumXml, evaluatedValueEscaped, null /* predecessor unknown */);
}
if (!propagateMetadataToSiblingItems)
{
_directMetadata ??= new PropertyDictionary<ProjectMetadata>();
_directMetadata.Set(metadatum);
}
else
{
var siblingItems = _project.Items.Where(i => i._xml == _xml);
foreach (var siblingItem in siblingItems)
{
siblingItem._directMetadata ??= new PropertyDictionary<ProjectMetadata>();
siblingItem._directMetadata.Set(metadatum.DeepClone());
}
}
return metadatum;
}
/// <summary>
/// Removes any metadata with the specified name.
/// Returns true if the evaluated metadata existed, otherwise false.
/// If the metadata name is one of the built-in metadata, like "FullPath", throws InvalidArgumentException.
/// If the metadata originates in an item definition, and was not overridden, throws InvalidOperationException.
/// </summary>
public bool RemoveMetadata(string name)
{
if (Link != null)
{
return Link.RemoveMetadata(name);
}
ErrorUtilities.VerifyThrowArgumentLength(name);
ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name);
Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject);
ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent?.Parent != null, "OM_ObjectIsNoLongerActive");
ProjectMetadata metadatum = _directMetadata?[name];
if (metadatum == null)
{
ProjectMetadata itemDefinitionMetadata = GetItemDefinitionMetadata(name);
ErrorUtilities.VerifyThrowInvalidOperation(itemDefinitionMetadata == null, "OM_CannotRemoveMetadataOriginatingFromItemDefinition", name);
return false;
}
_project.SplitItemElementIfNecessary(_xml);
// New metadata objects may have been created
metadatum = _directMetadata[name];
_xml.RemoveChild(metadatum.Xml);
_directMetadata.Remove(name);
return true;
}
/// <summary>
/// Renames the item.
/// Equivalent to setting the <see cref="UnevaluatedInclude"/> value.
/// Generally, no expansion occurs. This is because it would potentially result in several items,
/// which is not meaningful semantics when renaming a single item.
/// However if the item does not need to be split (which would invalidate its ProjectItemElement),
/// and the new value expands to exactly one item, then its evaluated include is updated
/// with the expanded value, rather than the unexpanded value.
/// </summary>
/// <remarks>
/// Even if the new value expands to zero items, we do not expand it.
/// The common case we are interested in for expansion here is setting something
/// like "$(sourcesroot)\foo.cs" and expanding that to a single item.
/// If say "@(foo)" is set as the new name, and it expands to blank, that might
/// be surprising to the host and maybe even unhandled, if on full reevaluation
/// it wouldn’t expand to blank. That’s why we're being cautious and supporting
/// the most common scenario only.
/// Many hosts will do a ReevaluateIfNecessary before reading anyway.
/// </remarks>
public void Rename(string name)
{
if (Link != null)
{
Link.Rename(name);
return;
}
Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject);
ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent?.Parent != null, "OM_ObjectIsNoLongerActive");
if (String.Equals(UnevaluatedInclude, name, StringComparison.Ordinal))
{
return;
}
_fullPath = null; // Clear cached value
if (_xml.Count == 0 /* no metadata */ && _project.IsSuitableExistingItemXml(_xml, name, null /* no metadata */) && !FileMatcher.HasWildcardsSemicolonItemOrPropertyReferences(name))
{
_evaluatedIncludeEscaped = name;
// Fast item lookup tables are invalid now.
// Make sure that when the caller invokes ReevaluateIfNecessary() that they'll be refreshed.
Project.MarkDirty();
return;
}
bool splitOccurred = _project.SplitItemElementIfNecessary(_xml);
_xml.Include = name;
if (splitOccurred)
{
_evaluatedIncludeEscaped = name;
}
else
{
_evaluatedIncludeEscaped = _project.ExpandItemIncludeBestEffortLeaveEscaped(_xml);
}
}
#region IMetadataTable Members
/// <summary>
/// Retrieves any value we have in our metadata table for the metadata name specified.
/// If no value is available, returns empty string.
/// Value, if escaped, remains escaped.
/// </summary>
string IMetadataTable.GetEscapedValue(string name)
{
string value = ((IMetadataTable)this).GetEscapedValue(null, name);
return value;
}
/// <summary>
/// Retrieves any value we have in our metadata table for the metadata name and item type specified.
/// If no value is available, returns empty string.
/// If item type is null, it is ignored, otherwise it must match.
/// Value, if escaped, remains escaped.
/// </summary>
string IMetadataTable.GetEscapedValue(string itemType, string name)
{
string value = ((IMetadataTable)this).GetEscapedValueIfPresent(itemType, name);
return value ?? String.Empty;
}
/// <summary>
/// Returns the value if it exists.
/// If no value is available, returns null.
/// If item type is null, it is ignored, otherwise it must match.
/// Value, if escaped, remains escaped.
/// </summary>
string IMetadataTable.GetEscapedValueIfPresent(string itemType, string name)
{
if (itemType == null || MSBuildNameIgnoreCaseComparer.Default.Equals(ItemType, itemType))
{
string value = ((IItem)this).GetMetadataValueEscaped(name);
if (value.Length > 0 || HasMetadata(name))
{
return value;
}
}
return null;
}
#endregion
/// <summary>
/// Changes the item type of this item.
/// Until reevaluation puts it in the correct place, it will be placed at
/// the end of the list of items of its new type.
/// </summary>
/// <remarks>
/// This is a little involved, as it requires replacing
/// the XmlElement, and updating the project's datastructures.
/// </remarks>
internal void ChangeItemType(string newItemType)
{
ErrorUtilities.VerifyThrowArgumentLength(newItemType, "ItemType");
Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject);
ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent?.Parent != null, "OM_ObjectIsNoLongerActive");
if (String.Equals(ItemType, newItemType, StringComparison.Ordinal))
{
return;
}
_project.SplitItemElementIfNecessary(_xml);
_project.RemoveItemBeforeItemTypeChange(this);
// xml.ChangeItemType will throw if new item type is invalid. Make sure we re-add the item anyway
try
{
_xml.ChangeItemType(newItemType);
}
finally
{
_project.ReAddExistingItemAfterItemTypeChange(this);
}
}
/// <summary>
/// Creates new xml objects for itself, disconnecting from the old xml objects.
/// Called ONLY by <see cref="Microsoft.Build.Evaluation.Project.SplitItemElementIfNecessary(ProjectItemElement)"/>
/// </summary>
/// <remarks>
/// Called when breaking up a single ProjectItemElement that evaluates into several ProjectItems.
/// </remarks>
internal void SplitOwnItemElement()
{
ProjectItemElement oldXml = _xml;
_xml = _xml.ContainingProject.CreateItemElement(ItemType, ((IItem)this).EvaluatedIncludeEscaped);
oldXml.Parent.InsertBeforeChild(_xml, oldXml);
if (_directMetadata == null)
{
return;
}
// ProjectMetadata objects may be being shared with other ProjectItem objects,
// or originate from item definitions, so it is necessary to replace ours with
// new ones.
List<ProjectMetadata> temporary = new List<ProjectMetadata>(_directMetadata.Count);
foreach (ProjectMetadata metadatum in _directMetadata)
{
temporary.Add(metadatum);
}
_directMetadata = new PropertyDictionary<ProjectMetadata>(_directMetadata.Count);
foreach (ProjectMetadata metadatum in temporary)
{
SetMetadataValue(metadatum.Name, metadatum.EvaluatedValueEscaped);
}
}
/// <summary>
/// Helper to get the value of a built-in metadatum with
/// the specified name, if any.
/// </summary>
private string GetBuiltInMetadataEscaped(string name)
{
string value = null;
if (FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name))
{
value = BuiltInMetadata.GetMetadataValueEscaped(_project.DirectoryPath, _evaluatedIncludeBeforeWildcardExpansionEscaped, _evaluatedIncludeEscaped, this.Xml.ContainingProject.FullPath, name, ref _fullPath);
}
return value;
}
/// <summary>
/// Retrieves the named metadata from the item definition, if any.
/// If it is not present, returns null
/// </summary>
/// <param name="name">The metadata name.</param>
/// <returns>The value if it exists, null otherwise.</returns>
private ProjectMetadata GetItemDefinitionMetadata(string name)
{
ProjectMetadata metadataFromDefinition = null;
// Check any inherited item definition metadata first. It's more like
// direct metadata, but we didn't want to copy the tables.
if (_inheritedItemDefinitions != null)
{
foreach (ProjectItemDefinition inheritedItemDefinition in _inheritedItemDefinitions)
{
metadataFromDefinition = inheritedItemDefinition.GetMetadata(name);
if (metadataFromDefinition != null)
{
return metadataFromDefinition;
}
}
}
// Now try regular item definition metadata for this item type.
ProjectItemDefinition itemDefinition;
if (_project.ItemDefinitions.TryGetValue(ItemType, out itemDefinition))
{
metadataFromDefinition = itemDefinition.GetMetadata(name);
}
return metadataFromDefinition;
}
/// <summary>
/// A class factory for ProjectItems.
/// </summary>
internal class ProjectItemFactory : IItemFactory<ProjectItem, ProjectItem>
{
/// <summary>
/// The Project with which each item should be associated.
/// </summary>
private readonly Project _project;
/// <summary>
/// The project item's XML
/// </summary>
private ProjectItemElement _xml;
/// <summary>
/// Creates an item factory which does not specify an item xml. The item xml must
/// be specified later.
/// </summary>
/// <param name="project">The project for items generated.</param>
internal ProjectItemFactory(Project project)
{
_project = project;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="project">The project for items generated.</param>
/// <param name="xml">The xml for items generated.</param>
internal ProjectItemFactory(Project project, ProjectItemElement xml)
{
_project = project;
_xml = xml;
}
/// <summary>
/// Item type that items created by this factory will have.
/// </summary>
public string ItemType
{
get { return _xml.ItemType; }
set { ErrorUtilities.ThrowInternalError("Cannot change the item type on ProjectItem.ProjectItemFactory"); }
}
/// <summary>
/// Set the item xml from which items will be created.
/// Used by the evaluator only.
/// </summary>
public ProjectItemElement ItemElement
{
set { _xml = value; }
}
/// <summary>
/// Creates an item with the specified type and evaluated include.
/// Used for making items from "just strings" and from expressions like "@(Compile, ';')"
/// </summary>
/// <param name="include">The include.</param>
/// <param name="definingProject">The path to the project that defined the item.</param>
/// <returns>A new project item.</returns>
/// <comments>
/// NOTE: defining project is ignored because we already know the ItemElement associated with
/// this item, and use that for where it is defined.
/// </comments>
public ProjectItem CreateItem(string include, string definingProject)
{
return CreateItem(include, include, definingProject);
}
/// <summary>
/// Creates an item based on the provided item, but with
/// the project and xml of this factory. Metadata is cloned,
/// but continues to point to the original ProjectMetadataElement objects.
/// This is to support the scenario Include="@(i)" where we are copying
/// metadata, and are happy to see changes in the original metadata, but
/// setting metadata should create new XML.
/// </summary>
/// <comments>
/// NOTE: defining project is ignored because we already know the ItemElement associated with
/// this item, and use that for where it is defined.
/// </comments>
public ProjectItem CreateItem(ProjectItem source, string definingProject)
{
return CreateItem(source._evaluatedIncludeEscaped, source._evaluatedIncludeBeforeWildcardExpansionEscaped, source);
}
/// <summary>
/// Creates an item based on the provided item, but with
/// the project and xml of this factory and the specified include. Metadata is cloned,
/// but continues to point to the original ProjectMetadataElement objects.
/// This is to support this scenario: Include="@(i->'xxx')"
/// </summary>
/// <remarks>
/// If the item type of the source is the same as the item type of the destination,
/// then it's not necessary to copy metadata originating in an item definition.
/// If it's not, we have to clone that too.
/// </remarks>
/// <comments>
/// NOTE: defining project is ignored because we already know the ItemElement associated with
/// this item, and use that for where it is defined.
/// </comments>
public ProjectItem CreateItem(string evaluatedIncludeEscaped, ProjectItem source, string definingProject)
{
return CreateItem(evaluatedIncludeEscaped, evaluatedIncludeEscaped, source);
}
/// <summary>
/// Creates an item with the specified include and include before wildcard expansion.
/// This is to support creating items from an include that may have a wildcard expression in it.
/// </summary>
/// <comments>
/// NOTE: defining project is ignored because we already know the ItemElement associated with
/// this item, and use that for where it is defined.
/// </comments>
public ProjectItem CreateItem(string evaluatedIncludeEscaped, string evaluatedIncludeBeforeWildcardExpansion, string definingProject)
{
ErrorUtilities.VerifyThrowInternalNull(_xml, "xml");
return new ProjectItem(_project, _xml, evaluatedIncludeEscaped, evaluatedIncludeBeforeWildcardExpansion, null /* no metadata */, null /* no inherited definition metadata */);
}
/// <summary>
/// Applies the supplied metadata to the destination item.
/// </summary>
public void SetMetadata(IEnumerable<KeyValuePair<ProjectMetadataElement, string>> metadata, IEnumerable<ProjectItem> destinationItems)
{
foreach (IItem<ProjectMetadata> item in destinationItems)
{
foreach (KeyValuePair<ProjectMetadataElement, string> metadatum in metadata)
{
item.SetMetadata(metadatum.Key, metadatum.Value);
}
}
}
/// <summary>
/// Creates an item based on the provided item, with the specified include and item type.
/// </summary>
private ProjectItem CreateItem(string evaluatedIncludeEscaped, string evaluatedIncludeBeforeWildcardExpansionEscaped, ProjectItem source)
{
ErrorUtilities.VerifyThrowInternalNull(_xml, "xml");
// The new item inherits any metadata originating in item definitions, which
// takes precedence over its own item definition metadata.
//
// Order of precedence:
// (1) any directly defined metadata on the source item
// (2) any inherited item definition metadata the source item had accumulated, in order of accumulation
// (3) any item definition metadata associated with the source item's item type
// (4) any item definition metadata associated with the destination item's item type; none yet.
// Clone for (1)
PropertyDictionary<ProjectMetadata> directMetadataClone = null;
if (source.DirectMetadataCount > 0)
{
directMetadataClone = new PropertyDictionary<ProjectMetadata>(source.DirectMetadataCount);
foreach (ProjectMetadata metadatum in source._directMetadata)
{
directMetadataClone.Set(metadatum.DeepClone());
}
}
// Combine (2) and (3) into a list, (2) on top.
int inheritedItemDefinitionsCount = (source._inheritedItemDefinitions == null) ? 0 : source._inheritedItemDefinitions.Count;
List<ProjectItemDefinition> inheritedItemDefinitionsClone = null;
if (source._inheritedItemDefinitions != null)
{
inheritedItemDefinitionsClone ??= new List<ProjectItemDefinition>(inheritedItemDefinitionsCount + 1);
inheritedItemDefinitionsClone.AddRange(source._inheritedItemDefinitions);
}
ProjectItemDefinition sourceItemDefinition;
if (_project.ItemDefinitions.TryGetValue(source.ItemType, out sourceItemDefinition))
{
inheritedItemDefinitionsClone ??= new List<ProjectItemDefinition>(inheritedItemDefinitionsCount + 1);
inheritedItemDefinitionsClone.Add(sourceItemDefinition);
}
return new ProjectItem(_project, _xml, evaluatedIncludeEscaped, evaluatedIncludeBeforeWildcardExpansionEscaped, directMetadataClone, inheritedItemDefinitionsClone);
}
}
/// <summary>
/// Implementation of IMetadataTable that can be passed to expander
/// to expose only built-in metadata on this item.
/// </summary>
private class BuiltInMetadataTable : IMetadataTable
{
/// <summary>
/// Backing item
/// </summary>
private ProjectItem _item;
/// <summary>
/// Constructor.
/// </summary>
internal BuiltInMetadataTable(ProjectItem item)
{
_item = item;
}
/// <summary>
/// Retrieves any value we have in our metadata table for the metadata name specified.
/// If no value is available, returns empty string.
/// </summary>
public string GetEscapedValue(string name)
{
string value = _item.GetBuiltInMetadataEscaped(name);
return value;
}
/// <summary>
/// Retrieves any value we have in our metadata table for the metadata name and item type specified.
/// If item type is null, it is ignored.
/// If no value is available, returns empty string.
/// </summary>
public string GetEscapedValue(string itemType, string name)
{
string value = GetEscapedValueIfPresent(itemType, name);
return value ?? String.Empty;
}
/// <summary>
/// Returns the value if it exists, null otherwise.
/// If item type is null, it is ignored.
/// </summary>
public string GetEscapedValueIfPresent(string itemType, string name)
{
string value = null;
if ((itemType == null) || String.Equals(_item.ItemType, itemType, StringComparison.OrdinalIgnoreCase))
{
value = GetEscapedValue(name);
}
return value;
}
}
}
}
|