File: Construction\ProjectElement.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.Diagnostics;
using System.Xml;
using Microsoft.Build.Framework;
using Microsoft.Build.ObjectModelRemoting;
using Microsoft.Build.Shared;
using ProjectXmlUtilities = Microsoft.Build.Internal.ProjectXmlUtilities;
 
#nullable disable
 
namespace Microsoft.Build.Construction
{
    /// <summary>
    /// Abstract base class for MSBuild construction object model elements.
    /// </summary>
    public abstract class ProjectElement : IProjectElement, ILinkableObject
    {
        /// <summary>
        /// Parent container object.
        /// </summary>
        private ProjectElementContainer _parent;
 
        /// <summary>
        /// Condition value cached for performance
        /// </summary>
        private string _condition;
 
        private bool _expressedAsAttribute;
        private ProjectElement _previousSibling;
        private ProjectElement _nextSibling;
 
        /// <summary>
        /// Constructor called by ProjectRootElement only.
        /// XmlElement is set directly after construction.
        /// </summary>
        /// <comment>
        /// Should be protected+internal.
        /// </comment>
        internal ProjectElement()
        {
        }
 
        /// <summary>
        /// External projects support
        /// </summary>
        internal ProjectElement(ProjectElementLink link)
        {
            ErrorUtilities.VerifyThrowArgumentNull(link);
 
            _xmlSource = link;
        }
 
        /// <summary>
        /// Constructor called by derived classes, except from ProjectRootElement.
        /// Parameters may not be null, except parent.
        /// </summary>
        internal ProjectElement(XmlElement xmlElement, ProjectElementContainer parent, ProjectRootElement containingProject)
        {
            ErrorUtilities.VerifyThrowArgumentNull(xmlElement);
            ErrorUtilities.VerifyThrowArgumentNull(containingProject);
 
            _xmlSource = (XmlElementWithLocation)xmlElement;
            _parent = parent;
            ContainingProject = containingProject;
        }
 
        /// <summary>
        /// Allows data (for example, item metadata) to be represented as an attribute on the parent element instead of as a child element.
        /// </summary>
        /// <remarks>
        /// If this is true, then the <see cref="XmlElement"/> will still be used to hold the data for this (pseudo) ProjectElement, but
        /// it will not be added to the Xml tree.
        /// </remarks>
        internal virtual bool ExpressedAsAttribute
        {
            get => Link != null ? Link.ExpressedAsAttribute : _expressedAsAttribute;
            set
            {
                if (Link != null)
                {
                    Link.ExpressedAsAttribute = value;
                }
                else if (value != _expressedAsAttribute)
                {
                    Parent?.RemoveFromXml(this);
                    _expressedAsAttribute = value;
                    Parent?.AddToXml(this);
                    MarkDirty("Set express as attribute: {0}", value.ToString());
                }
            }
        }
 
        /// <summary>
        /// Gets or sets the Condition value.
        /// It will return empty string IFF a condition attribute is legal but it’s not present or has no value.
        /// It will return null IFF a Condition attribute is illegal on that element.
        /// Removes the attribute if the value to set is empty.
        /// It is possible for derived classes to throw an <see cref="InvalidOperationException"/> if setting the condition is
        /// not applicable for those elements.
        /// </summary>
        /// <example> For the "ProjectExtensions" element, the getter returns null and the setter
        /// throws an exception for any value. </example>
        public virtual string Condition
        {
            [DebuggerStepThrough]
            get
            {
                return GetAttributeValue(XMakeAttributes.condition, ref _condition);
            }
 
            [DebuggerStepThrough]
            set
            {
                SetOrRemoveAttribute(XMakeAttributes.condition, value, ref _condition, "Set condition {0}", value);
            }
        }
 
        /// <summary>
        /// Gets or sets the Label value.
        /// Returns empty string if it is not present.
        /// Removes the attribute if the value to set is empty.
        /// </summary>
        public string Label
        {
            [DebuggerStepThrough]
            get
            {
                return GetAttributeValue(XMakeAttributes.label);
            }
 
            [DebuggerStepThrough]
            set
            {
                SetOrRemoveAttribute(XMakeAttributes.label, value, "Set label {0}", value);
            }
        }
 
        /// <summary>
        /// Null if this is a ProjectRootElement.
        /// Null if this has not been attached to a parent yet.
        /// </summary>
        /// <remarks>
        /// Parent should only be set by ProjectElementContainer.
        /// </remarks>
        public ProjectElementContainer Parent
        {
            [DebuggerStepThrough]
            get
            {
                if (this.Link != null) { return this.Link.Parent; }
 
                if (_parent is WrapperForProjectRootElement)
                {
                    // We hijacked the field to store the owning PRE. This element is actually unparented.
                    return null;
                }
 
                return _parent;
            }
 
            internal set
            {
                ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed.");
                if (value == null)
                {
                    // We're about to lose the parent. Hijack the field to store the owning PRE.
                    _parent = new WrapperForProjectRootElement(ContainingProject);
                }
                else
                {
                    _parent = value;
                }
 
                OnAfterParentChanged(value);
            }
        }
 
        /// <inheritdoc/>
        public string OuterElement => Link != null ? Link.OuterElement : XmlElement.OuterXml;
 
        /// <summary>
        /// All parent elements of this element, going up to the ProjectRootElement.
        /// None if this itself is a ProjectRootElement.
        /// None if this itself has not been attached to a parent yet.
        /// </summary>
        public IEnumerable<ProjectElementContainer> AllParents
        {
            get
            {
                ProjectElementContainer currentParent = Parent;
                while (currentParent != null)
                {
                    yield return currentParent;
                    currentParent = currentParent.Parent;
                }
            }
        }
 
        /// <summary>
        /// Previous sibling element.
        /// May be null.
        /// </summary>
        /// <remarks>
        /// Setter should ideally be "protected AND internal"
        /// </remarks>
        public ProjectElement PreviousSibling
        {
            [DebuggerStepThrough]
            get => Link != null ? Link.PreviousSibling : _previousSibling;
            [DebuggerStepThrough]
            internal set => _previousSibling = value;
        }
 
        /// <summary>
        /// Next sibling element.
        /// May be null.
        /// </summary>
        /// <remarks>
        /// Setter should ideally be "protected AND internal"
        /// </remarks>
        public ProjectElement NextSibling
        {
            [DebuggerStepThrough]
            get => Link != null ? Link.NextSibling : _nextSibling;
            [DebuggerStepThrough]
            internal set => _nextSibling = value;
        }
 
        /// <summary>
        /// ProjectRootElement (possibly imported) that contains this Xml.
        /// Cannot be null.
        /// </summary>
        /// <remarks>
        /// Setter ideally would be "protected and internal"
        /// There are some tricks here in order to save the space of a field: there are a lot of these objects.
        /// </remarks>
        public ProjectRootElement ContainingProject
        {
            get
            {
                if (Link != null)
                {
                    return Link.ContainingProject;
                }
 
                // If this element is unparented, we have hijacked the 'parent' field and stored the owning PRE in a special wrapper; get it from that.
                if (_parent is WrapperForProjectRootElement wrapper)
                {
                    return wrapper.ContainingProject;
                }
 
                // If this element is parented, the parent field is the true parent, and we ask that for the PRE.
                // It will call into this same getter on itself and figure it out.
                return Parent.ContainingProject;
            }
 
            // ContainingProject is set ONLY when an element is first constructed.
            internal set
            {
                ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed.");
                ErrorUtilities.VerifyThrowArgumentNull(value, "ContainingProject");
 
                if (_parent == null)
                {
                    // Not parented yet, hijack the field to store the ContainingProject
                    _parent = new WrapperForProjectRootElement(value);
                }
            }
        }
 
        /// <summary>
        /// Location of the "Condition" attribute on this element, if any.
        /// If there is no such attribute, returns null.
        /// </summary>
        public virtual ElementLocation ConditionLocation => GetAttributeLocation(XMakeAttributes.condition);
 
        /// <summary>
        /// Location of the "Label" attribute on this element, if any.
        /// If there is no such attribute, returns null;
        /// </summary>
        public ElementLocation LabelLocation => GetAttributeLocation(XMakeAttributes.label);
 
        /// <summary>
        /// Location of the corresponding Xml element.
        /// May not be correct if file is not saved, or
        /// file has been edited since it was last saved.
        /// In the case of an unsaved edit, the location only
        /// contains the path to the file that the element originates from.
        /// </summary>
        public ElementLocation Location => Link != null ? Link.Location : XmlElement.Location;
 
        /// <inheritdoc/>
        public string ElementName => Link != null ? Link.ElementName : XmlElement.Name;
 
        // Using ILinkedXml to share single field for either Linked (external) and local (XML backed) nodes.
        private ILinkedXml _xmlSource;
 
        internal ProjectElementLink Link => _xmlSource?.Link;
 
        /// <summary>
        /// <see cref="ILinkableObject.Link"/>
        /// </summary>
        object ILinkableObject.Link => Link;
 
 
        /// <summary>
        /// Gets the XmlElement associated with this project element.
        /// The setter is used when adding new elements.
        /// Never null except during load or creation.
        /// </summary>
        /// <remarks>
        /// This should be protected, but "protected internal" means "OR" not "AND",
        /// so this is not possible.
        /// </remarks>
        internal XmlElementWithLocation XmlElement => _xmlSource?.Xml;
 
        /// <summary>
        /// Gets the XmlDocument associated with this project element.
        /// </summary>
        /// <remarks>
        /// Never null except during load or creation.
        /// This should be protected, but "protected internal" means "OR" not "AND",
        /// so this is not possible.
        /// </remarks>
        internal XmlDocumentWithLocation XmlDocument
        {
            [DebuggerStepThrough]
            get
            {
                return (XmlDocumentWithLocation)XmlElement?.OwnerDocument;
            }
        }
 
        /// <summary>
        /// Returns a shallow clone of this project element.
        /// </summary>
        /// <returns>The cloned element.</returns>
        public ProjectElement Clone()
        {
            return Clone(ContainingProject);
        }
 
        /// <summary>
        /// Applies properties from the specified type to this instance.
        /// </summary>
        /// <param name="element">The element to act as a template to copy from.</param>
        public virtual void CopyFrom(ProjectElement element)
        {
            ErrorUtilities.VerifyThrowArgumentNull(element);
            ErrorUtilities.VerifyThrowArgument(GetType().IsEquivalentTo(element.GetType()), "CannotCopyFromElementOfThatType");
 
            if (this == element)
            {
                return;
            }
 
            if (Link != null)
            {
                Link.CopyFrom(element);
                return;
            }
 
            // Remove all the current attributes and textual content.
            XmlElement.RemoveAllAttributes();
            if (XmlElement.ChildNodes.Count == 1 && XmlElement.FirstChild.NodeType == XmlNodeType.Text)
            {
                XmlElement.RemoveChild(XmlElement.FirstChild);
            }
 
            // Ensure the element name itself matches.
            ReplaceElement(XmlUtilities.RenameXmlElement(XmlElement, element.ElementName, XmlElement.NamespaceURI));
 
            // hard case when argument is a linked object (slight duplication).
            if (element.Link != null)
            {
                foreach (var remoteAttribute in element.Link.Attributes)
                {
                    if (ShouldCloneXmlAttribute(remoteAttribute))
                    {
                        XmlElement.SetAttribute(remoteAttribute.LocalName, remoteAttribute.NamespaceURI, remoteAttribute.Value);
                    }
                }
                var pureText = element.Link.PureText;
                if (pureText != null)
                {
                    XmlElement.AppendChild(XmlElement.OwnerDocument.CreateTextNode(pureText));
                }
 
                _expressedAsAttribute = element.ExpressedAsAttribute;
            }
            else
            {
                // Copy over the attributes from the template element.
                foreach (XmlAttribute attribute in element.XmlElement.Attributes)
                {
                    if (ShouldCloneXmlAttribute(attribute))
                    {
                        XmlElement.SetAttribute(attribute.LocalName, attribute.NamespaceURI, attribute.Value);
                    }
                }
 
                // If this element has pure text content, copy that over.
                if (element.XmlElement.ChildNodes.Count == 1 && element.XmlElement.FirstChild.NodeType == XmlNodeType.Text)
                {
                    XmlElement.AppendChild(XmlElement.OwnerDocument.CreateTextNode(element.XmlElement.FirstChild.Value));
                }
 
                _expressedAsAttribute = element._expressedAsAttribute;
            }
 
            MarkDirty("CopyFrom", null);
            ClearAttributeCache();
        }
 
        /// <summary>
        /// Hook for subclasses to specify whether the given <paramref name="attribute"></paramref> should be cloned or not
        /// </summary>
        protected virtual bool ShouldCloneXmlAttribute(XmlAttribute attribute) => true;
 
        internal virtual bool ShouldCloneXmlAttribute(XmlAttributeLink attributeLink) => true;
 
        /// <summary>
        /// Called only by the parser to tell the ProjectRootElement its backing XmlElement and its own parent project (itself)
        /// This can't be done during construction, as it hasn't loaded the document at that point and it doesn't have a 'this' pointer either.
        /// </summary>
        internal void SetProjectRootElementFromParser(XmlElementWithLocation xmlElement, ProjectRootElement projectRootElement)
        {
            _xmlSource = xmlElement;
            ContainingProject = projectRootElement;
        }
 
        /// <summary>
        /// Called by ProjectElementContainer to clear the parent when
        /// removing an element from its parent.
        /// </summary>
        internal void ClearParent()
        {
            Parent = null;
        }
 
        /// <summary>
        /// Called by a DERIVED CLASS to indicate its XmlElement has changed.
        /// This normally shouldn't happen, so it's broken out into an explicit method.
        /// An example of when it has to happen is when an item's type is changed.
        /// We trust the caller to have fixed up the XmlDocument properly.
        /// We ASSUME that attributes were copied verbatim. If this is not the case,
        /// any cached attribute values would have to be cleared.
        /// If the new element is actually the existing element, does nothing, and does
        /// not mark the project dirty.
        /// </summary>
        /// <remarks>
        /// This should be protected, but "protected internal" means "OR" not "AND",
        /// so this is not possible.
        /// </remarks>
        internal void ReplaceElement(XmlElementWithLocation newElement)
        {
            if (ReferenceEquals(newElement, XmlElement))
            {
                return;
            }
 
            _xmlSource = newElement;
            MarkDirty("Replace element {0}", newElement.Name);
        }
 
        /// <summary>
        /// Overridden to verify that the potential parent and siblings
        /// are acceptable. Throws InvalidOperationException if they are not.
        /// </summary>
        internal abstract void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer proposedParent, ProjectElement previousSibling, ProjectElement nextSibling);
 
        /// <summary>
        /// Marks this element as dirty.
        /// The default implementation simply marks the parent as dirty.
        /// If there is no parent, because the element has not been parented, do nothing. The parent
        /// will be dirtied when the element is added.
        /// Accepts a reason for debugging purposes only, and optional reason parameter.
        /// </summary>
        /// <comment>
        /// Should ideally be protected+internal.
        /// </comment>
        internal virtual void MarkDirty(string reason, string param)
        {
            Parent?.MarkDirty(reason, param);
        }
 
        /// <summary>
        /// Called after a new parent is set. Parent may be null.
        /// By default does nothing.
        /// </summary>
        internal virtual void OnAfterParentChanged(ProjectElementContainer newParent)
        {
        }
 
        /// <summary>
        /// Returns a shallow clone of this project element.
        /// </summary>
        /// <param name="factory">The factory to use for creating the new instance.</param>
        /// <returns>The cloned element.</returns>
        protected internal virtual ProjectElement Clone(ProjectRootElement factory)
        {
            var clone = CreateNewInstance(factory);
            if (!clone.GetType().IsEquivalentTo(GetType()))
            {
                ErrorUtilities.ThrowInternalError("{0}.Clone() returned an instance of type {1}.", GetType().Name, clone.GetType().Name);
            }
 
            clone.CopyFrom(this);
            return clone;
        }
 
        /// <summary>
        /// Returns a new instance of this same type.
        /// Any properties that cannot be set after creation should be set to copies of values
        /// as set for this instance.
        /// </summary>
        /// <param name="owner">The factory to use for creating the new instance.</param>
        protected abstract ProjectElement CreateNewInstance(ProjectRootElement owner);
 
        internal static ProjectElement CreateNewInstance(ProjectElement xml, ProjectRootElement owner)
        {
            if (xml.Link != null)
            {
                return xml.Link.CreateNewInstance(owner);
            }
 
            return xml.CreateNewInstance(owner);
        }
 
        internal ElementLocation GetAttributeLocation(string attributeName)
        {
            return Link != null ? Link.GetAttributeLocation(attributeName) : XmlElement.GetAttributeLocation(attributeName);
        }
 
        internal string GetAttributeValue(string attributeName, bool nullIfNotExists = false)
        {
            return Link != null ? Link.GetAttributeValue(attributeName, nullIfNotExists) :
                ProjectXmlUtilities.GetAttributeValue(XmlElement, attributeName, nullIfNotExists);
        }
 
        internal string GetAttributeValue(string attributeName, ref string cache)
        {
            if (cache != null)
            {
                return cache;
            }
 
            var value = GetAttributeValue(attributeName, false);
            if (Link == null)
            {
                cache = value;
            }
 
            return value;
        }
 
        internal virtual void ClearAttributeCache()
        {
            this._condition = null;
        }
 
        internal void SetOrRemoveAttributeForLink(string name, string value, bool clearAttributeCache, string reason, string param)
        {
            SetOrRemoveAttribute(name, value, reason, param);
            if (clearAttributeCache)
            {
                this.ClearAttributeCache();
            }
        }
 
        internal void SetOrRemoveAttribute(string name, string value, string reason = null, string param = null)
        {
            if (Link != null)
            {
                Link.SetOrRemoveAttribute(name, value, false, reason, param);
            }
            else
            {
                ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, name, value);
                if (reason != null)
                {
                    MarkDirty(reason, param);
                }
            }
        }
 
        internal void SetOrRemoveAttribute(string name, string value, ref string cache, string reason = null, string param = null)
        {
            if (Link != null)
            {
                Link.SetOrRemoveAttribute(name, value, true, reason, param);
            }
            else
            {
                ProjectXmlUtilities.SetOrRemoveAttribute(XmlElement, name, value);
                cache = value;
                if (reason != null)
                {
                    MarkDirty(reason, param);
                }
            }
        }
 
 
        /// <summary>
        /// Special derived variation of ProjectElementContainer used to wrap a ProjectRootElement.
        /// This is part of a trick used in ProjectElement to avoid using a separate field for the containing PRE.
        /// </summary>
        private class WrapperForProjectRootElement : ProjectElementContainer
        {
            /// <summary>
            /// Constructor
            /// </summary>
            internal WrapperForProjectRootElement(ProjectRootElement containingProject)
            {
                ErrorUtilities.VerifyThrowInternalNull(containingProject);
                ContainingProject = containingProject;
            }
 
            /// <summary>
            /// Wrapped ProjectRootElement
            /// </summary>
            internal new ProjectRootElement ContainingProject { get; }
 
            /// <summary>
            /// Dummy required implementation
            /// </summary>
            internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling)
            {
                ErrorUtilities.ThrowInternalErrorUnreachable();
            }
 
            /// <inheritdoc />
            protected override ProjectElement CreateNewInstance(ProjectRootElement owner)
            {
                return new WrapperForProjectRootElement(ContainingProject);
            }
        }
    }
}