|
// 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.Collections.ObjectModel;
using System.Diagnostics;
using System.Xml;
using Microsoft.Build.Collections;
using Microsoft.Build.ObjectModelRemoting;
using Microsoft.Build.Shared;
#nullable disable
namespace Microsoft.Build.Construction
{
/// <summary>
/// ProjectTaskElement represents the Task element in the MSBuild project.
/// </summary>
[DebuggerDisplay("{Name} Condition={Condition} ContinueOnError={ContinueOnError} MSBuildRuntime={MSBuildRuntime} MSBuildArchitecture={MSBuildArchitecture} #Outputs={Count}")]
public class ProjectTaskElement : ProjectElementContainer
{
internal ProjectTaskElementLink TaskLink => (ProjectTaskElementLink)Link;
/// <summary>
/// External projects support
/// </summary>
internal ProjectTaskElement(ProjectTaskElementLink link)
: base(link)
{
}
/// <summary>
/// The parameters (excepting condition and continue-on-error)
/// </summary>
private CopyOnWriteDictionary<(string, ElementLocation)> _parameters;
/// <summary>
/// Protection for the parameters cache
/// </summary>
private readonly Object _locker = new Object();
/// <summary>
/// Initialize a parented ProjectTaskElement
/// </summary>
internal ProjectTaskElement(XmlElementWithLocation xmlElement, ProjectTargetElement parent, ProjectRootElement containingProject)
: base(xmlElement, parent, containingProject)
{
ErrorUtilities.VerifyThrowArgumentNull(parent);
}
/// <summary>
/// Initialize an unparented ProjectTaskElement
/// </summary>
private ProjectTaskElement(XmlElementWithLocation xmlElement, ProjectRootElement containingProject)
: base(xmlElement, null, containingProject)
{
}
/// <summary>
/// Gets or sets the continue on error value.
/// Returns empty string if it is not present.
/// Removes the attribute if the value to set is empty.
/// </summary>
public string ContinueOnError
{
[DebuggerStepThrough]
get
{
return GetAttributeValue(XMakeAttributes.continueOnError);
}
[DebuggerStepThrough]
set
{
SetOrRemoveAttribute(XMakeAttributes.continueOnError, value, "Set task ContinueOnError {0}", value);
}
}
/// <summary>
/// Gets or sets the runtime value for the task.
/// Returns empty string if it is not present.
/// Removes the attribute if the value to set is empty.
/// </summary>
public string MSBuildRuntime
{
[DebuggerStepThrough]
get
{
return GetAttributeValue(XMakeAttributes.msbuildRuntime);
}
[DebuggerStepThrough]
set
{
SetOrRemoveAttribute(XMakeAttributes.msbuildRuntime, value, "Set task MSBuildRuntime {0}", value);
}
}
/// <summary>
/// Gets or sets the architecture value for the task.
/// Returns empty string if it is not present.
/// Removes the attribute if the value to set is empty.
/// </summary>
public string MSBuildArchitecture
{
[DebuggerStepThrough]
get
{
return GetAttributeValue(XMakeAttributes.msbuildArchitecture);
}
[DebuggerStepThrough]
set
{
SetOrRemoveAttribute(XMakeAttributes.msbuildArchitecture, value, "Set task MSBuildArchitecture {0}", value);
}
}
/// <summary>
/// Gets the task name
/// </summary>
public string Name => ElementName;
/// <summary>
/// Gets any output children.
/// </summary>
public ICollection<ProjectOutputElement> Outputs => GetChildrenOfType<ProjectOutputElement>();
/// <summary>
/// Enumerable over the unevaluated parameters on the task.
/// Attributes with their own properties, such as ContinueOnError, are not included in this collection.
/// If parameters differ only by case only the last one will be returned. MSBuild uses only this one.
/// Hosts can still remove the other parameters by using RemoveAllParameters().
/// </summary>
public IDictionary<string, string> Parameters
{
get
{
if (Link != null)
{
return TaskLink.Parameters;
}
lock (_locker)
{
EnsureParametersInitialized();
var parametersClone = new Dictionary<string, string>(_parameters.Count, StringComparer.OrdinalIgnoreCase);
foreach (KeyValuePair<string, (string, ElementLocation)> entry in _parameters)
{
parametersClone[entry.Key] = entry.Value.Item1;
}
return new ReadOnlyDictionary<string, string>(parametersClone);
}
}
}
/// <summary>
/// Enumerable over the locations of parameters on the task.
/// Condition and ContinueOnError, which have their own properties, are not included in this collection.
/// If parameters differ only by case only the last one will be returned. MSBuild uses only this one.
/// Hosts can still remove the other parameters by using RemoveAllParameters().
/// </summary>
public IEnumerable<KeyValuePair<string, ElementLocation>> ParameterLocations
{
get
{
if (Link != null)
{
return TaskLink.ParameterLocations;
}
lock (_locker)
{
EnsureParametersInitialized();
var parameterLocations = new List<KeyValuePair<string, ElementLocation>>();
foreach (KeyValuePair<string, (string, ElementLocation)> entry in _parameters)
{
parameterLocations.Add(new KeyValuePair<string, ElementLocation>(entry.Key, entry.Value.Item2));
}
return parameterLocations;
}
}
}
/// <summary>
/// Location of the "ContinueOnError" attribute on this element, if any.
/// If there is no such attribute, returns null;
/// </summary>
public ElementLocation ContinueOnErrorLocation => GetAttributeLocation(XMakeAttributes.continueOnError);
/// <summary>
/// Location of the "MSBuildRuntime" attribute on this element, if any.
/// If there is no such attribute, returns null;
/// </summary>
public ElementLocation MSBuildRuntimeLocation => GetAttributeLocation(XMakeAttributes.msbuildRuntime);
/// <summary>
/// Location of the "MSBuildArchitecture" attribute on this element, if any.
/// If there is no such attribute, returns null;
/// </summary>
public ElementLocation MSBuildArchitectureLocation => GetAttributeLocation(XMakeAttributes.msbuildArchitecture);
/// <summary>
/// Retrieves a copy of the parameters as used during evaluation.
/// </summary>
internal CopyOnWriteDictionary<(string, ElementLocation)> ParametersForEvaluation
{
get
{
ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed.");
lock (_locker)
{
EnsureParametersInitialized();
return _parameters.Clone(); // copy on write!
}
}
}
/// <summary>
/// Convenience method to add an Output Item to this task.
/// Adds after the last child.
/// </summary>
public ProjectOutputElement AddOutputItem(string taskParameter, string itemType)
{
ErrorUtilities.VerifyThrowArgumentLength(taskParameter);
ErrorUtilities.VerifyThrowArgumentLength(itemType);
return AddOutputItem(taskParameter, itemType, null);
}
/// <summary>
/// Convenience method to add a conditioned Output Item to this task.
/// Adds after the last child.
/// </summary>
public ProjectOutputElement AddOutputItem(string taskParameter, string itemType, string condition)
{
ProjectOutputElement outputItem = ContainingProject.CreateOutputElement(taskParameter, itemType, null);
if (condition != null)
{
outputItem.Condition = condition;
}
AppendChild(outputItem);
return outputItem;
}
/// <summary>
/// Convenience method to add an Output Property to this task.
/// Adds after the last child.
/// </summary>
public ProjectOutputElement AddOutputProperty(string taskParameter, string propertyName)
{
ErrorUtilities.VerifyThrowArgumentLength(taskParameter);
ErrorUtilities.VerifyThrowArgumentLength(propertyName);
return AddOutputProperty(taskParameter, propertyName, null);
}
/// <summary>
/// Convenience method to add a conditioned Output Property to this task.
/// Adds after the last child.
/// </summary>
public ProjectOutputElement AddOutputProperty(string taskParameter, string propertyName, string condition)
{
ProjectOutputElement outputProperty = ContainingProject.CreateOutputElement(taskParameter, null, propertyName);
if (condition != null)
{
outputProperty.Condition = condition;
}
AppendChild(outputProperty);
return outputProperty;
}
/// <summary>
/// Gets the value of the parameter with the specified name,
/// or empty string if it is not present.
/// </summary>
public string GetParameter(string name)
{
if (Link != null)
{
return TaskLink.GetParameter(name);
}
lock (_locker)
{
ErrorUtilities.VerifyThrowArgumentLength(name);
EnsureParametersInitialized();
if (_parameters.TryGetValue(name, out (string, ElementLocation) parameter))
{
return parameter.Item1;
}
return String.Empty;
}
}
/// <summary>
/// Adds (or modifies the value of) a parameter on this task
/// </summary>
public void SetParameter(string name, string unevaluatedValue)
{
if (Link != null)
{
TaskLink.SetParameter(name, unevaluatedValue);
return;
}
lock (_locker)
{
ErrorUtilities.VerifyThrowArgumentLength(name);
ErrorUtilities.VerifyThrowArgumentNull(unevaluatedValue);
ErrorUtilities.VerifyThrowArgument(!XMakeAttributes.IsSpecialTaskAttribute(name), "CannotAccessKnownAttributes", name);
_parameters = null;
XmlElement.SetAttribute(name, unevaluatedValue);
MarkDirty("Set task parameter {0}", name);
}
}
/// <summary>
/// Removes any parameter on this task with the specified name.
/// If there is no such parameter, does nothing.
/// </summary>
public void RemoveParameter(string name)
{
if (Link != null)
{
TaskLink.RemoveParameter(name);
return;
}
lock (_locker)
{
_parameters = null;
XmlElement.RemoveAttribute(name);
MarkDirty("Remove task parameter {0}", name);
}
}
/// <summary>
/// Removes all parameters from the task.
/// Does not remove any "special" parameters: ContinueOnError, Condition, etc.
/// </summary>
public void RemoveAllParameters()
{
if (Link != null)
{
TaskLink.RemoveAllParameters();
return;
}
lock (_locker)
{
_parameters = null;
List<XmlAttribute> toRemove = null;
// note this was a long standing bug in here (which would make this only work if there is no attributes to remove).
// calling XmlElement.RemoveAttributeNode will cause foreach to throw ArgumentException (collection modified)
foreach (XmlAttribute attribute in XmlElement.Attributes)
{
if (!XMakeAttributes.IsSpecialTaskAttribute(attribute.Name))
{
toRemove ??= new List<XmlAttribute>();
toRemove.Add(attribute);
}
}
if (toRemove != null)
{
foreach (var attribute in toRemove)
{
XmlElement.RemoveAttributeNode(attribute);
}
MarkDirty("Remove all task parameters on {0}", Name);
}
}
}
/// <inheritdoc />
public override void CopyFrom(ProjectElement element)
{
base.CopyFrom(element);
// Clear caching fields
_parameters = null;
}
/// <summary>
/// Creates an unparented ProjectTaskElement, wrapping an unparented XmlElement.
/// Caller should then ensure the element is added to the XmlDocument in the appropriate location.
/// </summary>
/// <remarks>
/// Any legal XML element name is allowed. We can't easily verify if the name is a legal XML element name,
/// so this will specifically throw XmlException if it isn't.
/// </remarks>
internal static ProjectTaskElement CreateDisconnected(string name, ProjectRootElement containingProject)
{
ErrorUtilities.VerifyThrowArgumentLength(name);
XmlElementWithLocation element = containingProject.CreateElement(name);
return new ProjectTaskElement(element, containingProject);
}
/// <summary>
/// Overridden to verify that the potential parent and siblings
/// are acceptable. Throws InvalidOperationException if they are not.
/// </summary>
internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling)
{
ErrorUtilities.VerifyThrowInvalidOperation(parent is ProjectTargetElement, "OM_CannotAcceptParent");
}
/// <inheritdoc />
protected override ProjectElement CreateNewInstance(ProjectRootElement owner)
{
return owner.CreateTaskElement(Name);
}
/// <summary>
/// Initialize parameters cache.
/// Must be called within the lock.
/// </summary>
private void EnsureParametersInitialized()
{
if (_parameters == null)
{
_parameters = new CopyOnWriteDictionary<(string, ElementLocation)>(StringComparer.OrdinalIgnoreCase);
foreach (XmlAttributeWithLocation attribute in XmlElement.Attributes)
{
if (!XMakeAttributes.IsSpecialTaskAttribute(attribute.Name))
{
// By pulling off and caching the Location early here, it becomes frozen for the life of this object.
// That means that if the name of the file is changed after first load (possibly from null) it will
// remain the old value here. Correctly, this should cache the attribute not the location. Fixing
// that will need profiling, though, as this cache was added for performance.
_parameters[attribute.Name] = (attribute.Value, attribute.Location);
}
}
}
}
}
}
|