File: Construction\ProjectTaskElement.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// 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);
                    }
                }
            }
        }
    }
}