File: XamlTaskFactory\TaskParser.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core)
// 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.Globalization;
using System.IO;
using System.Text;
using System.Xaml;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using XamlTypes = Microsoft.Build.Framework.XamlTypes;
 
#nullable disable
 
namespace Microsoft.Build.Tasks.Xaml
{
    /// <summary>
    /// The TaskParser class takes an xml file and parses the parameters for a task.
    /// </summary>
    internal class TaskParser
    {
        /// <summary>
        /// The ordered list of how the switches get emitted.
        /// </summary>
        private readonly List<string> _switchOrderList = new List<string>();
 
        #region Properties
 
        /// <summary>
        /// The name of the task
        /// </summary>
        public string GeneratedTaskName { get; set; }
 
        /// <summary>
        /// The base type of the class
        /// </summary>
        public string BaseClass { get; } = "DataDrivenToolTask";
 
        /// <summary>
        /// The namespace of the class
        /// </summary>
        public string Namespace { get; } = "XamlTaskNamespace";
 
        /// <summary>
        /// Namespace for the resources
        /// </summary>
        public string ResourceNamespace { get; }
 
        /// <summary>
        /// The name of the executable
        /// </summary>
        public string ToolName { get; private set; }
 
        /// <summary>
        /// The default prefix for each switch
        /// </summary>
        public string DefaultPrefix { get; private set; } = String.Empty;
 
        /// <summary>
        /// All of the parameters that were parsed
        /// </summary>
        public LinkedList<Property> Properties { get; } = new LinkedList<Property>();
 
        /// <summary>
        /// All of the parameters that have a default value
        /// </summary>
        public LinkedList<Property> DefaultSet { get; } = new LinkedList<Property>();
 
        /// <summary>
        /// All of the properties that serve as fallbacks for unset properties
        /// </summary>
        public Dictionary<string, string> FallbackSet { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
        /// <summary>
        /// The ordered list of properties
        /// </summary>
        public IEnumerable<string> SwitchOrderList => _switchOrderList;
 
        /// <summary>
        /// Returns the log of errors
        /// </summary>
        public LinkedList<string> ErrorLog { get; } = new LinkedList<string>();
 
        #endregion
 
        /// <summary>
        /// Parse the specified string, either as a file path or actual XML content.
        /// </summary>
        public bool Parse(string contentOrFile, string desiredRule)
        {
            ErrorUtilities.VerifyThrowArgumentLength(contentOrFile);
            ErrorUtilities.VerifyThrowArgumentLength(desiredRule);
 
            bool parseSuccessful = ParseAsContentOrFile(contentOrFile, desiredRule);
            if (!parseSuccessful)
            {
                var parseErrors = new StringBuilder();
                parseErrors.AppendLine();
                foreach (string error in ErrorLog)
                {
                    parseErrors.AppendLine(error);
                }
 
                throw new ArgumentException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("Xaml.RuleParseFailed", parseErrors.ToString()));
            }
 
            return parseSuccessful;
        }
 
        private bool ParseAsContentOrFile(string contentOrFile, string desiredRule)
        {
            // On Windows:
            // - xml string will be an invalid file path, so, Path.GetFullPath will
            //   return null
            // - xml string cannot be a rooted path ("C:\\<abc />")
            //
            // On Unix:
            // - xml string is a valid path, this is not a definite check as Path.GetFullPath
            //   will return !null in most cases
            // - xml string cannot be a rooted path ("/foo/<abc />")
 
            bool isRootedPath = false;
            string maybeFullPath = null;
            try
            {
                isRootedPath = Path.IsPathRooted(contentOrFile);
                if (!isRootedPath)
                {
                    maybeFullPath = Path.GetFullPath(contentOrFile);
                }
            }
            catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
            {
                // We will get an exception if the contents are not a path (for instance, they are actual XML.)
            }
 
            if (isRootedPath)
            {
                // valid *absolute* file path
 
                if (!FileSystems.Default.FileExists(contentOrFile))
                {
                    throw new ArgumentException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("Xaml.RuleFileNotFound", contentOrFile));
                }
 
                using var sr = new StreamReader(contentOrFile);
 
                return ParseXamlDocument(sr, desiredRule);
            }
 
            // On Windows, xml content string is not a valid path, so, maybeFullPath == null
            // On Unix, xml content string would be a valid path, so, maybeFullPath != null
            if (maybeFullPath == null)
            {
                // Unable to convert to a path, parse as XML
                return ParseXamlDocument(new StringReader(contentOrFile), desiredRule);
            }
 
            if (FileSystems.Default.FileExists(maybeFullPath))
            {
                // file found, parse as a file
                using var sr = new StreamReader(maybeFullPath);
 
                return ParseXamlDocument(sr, desiredRule);
            }
 
            // @maybeFullPath is either:
            //  - a non-existent fullpath
            //  - or xml content with the current dir prepended (like "/foo/bar/<abc .. />"),
            //    but not on Windows
            //
            // On Windows, this means that @contentOrFile is really a non-existent file name
            if (NativeMethodsShared.IsWindows)
            {
                throw new ArgumentException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("Xaml.RuleFileNotFound", maybeFullPath));
            }
            else // On !Windows, try parsing as XML
            {
                return ParseXamlDocument(new StringReader(contentOrFile), desiredRule);
            }
        }
 
        /// <summary>
        /// Parse a Xaml document from a TextReader
        /// </summary>
        internal bool ParseXamlDocument(TextReader reader, string desiredRule)
        {
            ErrorUtilities.VerifyThrowArgumentNull(reader);
            ErrorUtilities.VerifyThrowArgumentLength(desiredRule);
 
            object rootObject = XamlServices.Load(reader);
            if (rootObject != null)
            {
                XamlTypes.ProjectSchemaDefinitions schemas = rootObject as XamlTypes.ProjectSchemaDefinitions;
                if (schemas != null)
                {
                    foreach (XamlTypes.IProjectSchemaNode node in schemas.Nodes)
                    {
                        XamlTypes.Rule rule = node as XamlTypes.Rule;
                        if (rule != null)
                        {
                            if (String.Equals(rule.Name, desiredRule, StringComparison.OrdinalIgnoreCase))
                            {
                                return ParseXamlDocument(rule);
                            }
                        }
                    }
 
                    throw new XamlParseException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("Xaml.RuleNotFound", desiredRule));
                }
                else
                {
                    throw new XamlParseException(ResourceUtilities.GetResourceString("Xaml.InvalidRootObject"));
                }
            }
 
            return false;
        }
 
        /// <summary>
        /// Parse a Xaml document from a rule
        /// </summary>
        internal bool ParseXamlDocument(XamlTypes.Rule rule)
        {
            if (rule == null)
            {
                return false;
            }
 
            DefaultPrefix = rule.SwitchPrefix;
 
            ToolName = rule.ToolName;
            GeneratedTaskName = rule.Name;
 
            // Dictionary of property name strings to property objects. If a property is in the argument list of the current property then we want to make sure
            // that the argument property is a dependency of the current property.
 
            // As properties are parsed they are added to this dictionary so that after we can find the property instances from the names quickly.
            var argumentDependencyLookup = new Dictionary<string, Property>(StringComparer.OrdinalIgnoreCase);
 
            // baseClass = attribute.InnerText;
            // namespaceValue = attribute.InnerText;
            // resourceNamespaceValue = attribute.InnerText;
            foreach (XamlTypes.BaseProperty property in rule.Properties)
            {
                if (!ParseParameterGroupOrParameter(property, Properties, null, argumentDependencyLookup /*Add to the dictionary properties as they are parsed*/))
                {
                    return false;
                }
            }
 
            // Go through each property and their arguments to set up the correct dependency mappings.
            foreach (Property property in Properties)
            {
                // Get the arguments on the property itself
                List<Argument> arguments = property.Arguments;
 
                // Find all of the properties in arguments list.
                foreach (Argument argument in arguments)
                {
                    if (argumentDependencyLookup.TryGetValue(argument.Parameter, out Property argumentProperty))
                    {
                        property.DependentArgumentProperties.AddLast(argumentProperty);
                    }
                }
 
                // Properties may be enumeration types, this would mean they have sub property values which themselves can have arguments.
                List<Value> values = property.Values;
 
                // Find all of the properties for the aruments in sub property.
                foreach (Value value in values)
                {
                    List<Argument> valueArguments = value.Arguments;
                    foreach (Argument argument in valueArguments)
                    {
                        if (argumentDependencyLookup.TryGetValue(argument.Parameter, out Property argumentProperty))
                        {
                            // If the property contains a value sub property that has a argument then we will declare that the original property has the same dependenecy.
                            property.DependentArgumentProperties.AddLast(argumentProperty);
                        }
                    }
                }
            }
 
            return true;
        }
 
        /// <summary>
        /// Reads in the nodes of the xml file one by one and builds the data structure of all existing properties
        /// </summary>
        private bool ParseParameterGroupOrParameter(XamlTypes.BaseProperty baseProperty, LinkedList<Property> propertyList, Property property, Dictionary<string, Property> argumentDependencyLookup)
        {
            // node is a property
            if (!ParseParameter(baseProperty, propertyList, property, argumentDependencyLookup))
            {
                return false;
            }
 
            return true;
        }
 
        /// <summary>
        /// Fills in the property data structure
        /// </summary>
        private bool ParseParameter(XamlTypes.BaseProperty baseProperty, LinkedList<Property> propertyList, Property property, Dictionary<string, Property> argumentDependencyLookup)
        {
            Property propertyToAdd = ObtainAttributes(baseProperty, property);
 
            if (String.IsNullOrEmpty(propertyToAdd.Name))
            {
                propertyToAdd.Name = "AlwaysAppend";
            }
 
            // generate the list of parameters in order
            if (!_switchOrderList.Contains(propertyToAdd.Name))
            {
                _switchOrderList.Add(propertyToAdd.Name);
            }
            else
            {
                throw new XamlParseException(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("Xaml.DuplicatePropertyName", propertyToAdd.Name));
            }
 
            // Inherit the Prefix from the Tool
            if (String.IsNullOrEmpty(propertyToAdd.Prefix))
            {
                propertyToAdd.Prefix = DefaultPrefix;
            }
 
            // If the property is an enum type, parse that.
            XamlTypes.EnumProperty enumProperty = baseProperty as XamlTypes.EnumProperty;
            if (enumProperty != null)
            {
                foreach (XamlTypes.EnumValue enumValue in enumProperty.AdmissibleValues)
                {
                    var value = new Value
                    {
                        Name = enumValue.Name,
                        SwitchName = enumValue.Switch
                    };
 
                    if (value.SwitchName == null)
                    {
                        value.SwitchName = String.Empty;
                    }
 
                    value.DisplayName = enumValue.DisplayName;
                    value.Description = enumValue.Description;
                    value.Prefix = enumValue.SwitchPrefix;
                    if (String.IsNullOrEmpty(value.Prefix))
                    {
                        value.Prefix = enumProperty.SwitchPrefix;
                    }
 
                    if (String.IsNullOrEmpty(value.Prefix))
                    {
                        value.Prefix = DefaultPrefix;
                    }
 
                    if (enumValue.Arguments.Count > 0)
                    {
                        value.Arguments = new List<Argument>();
                        foreach (XamlTypes.Argument argument in enumValue.Arguments)
                        {
                            var arg = new Argument
                            {
                                Parameter = argument.Property,
                                Separator = argument.Separator,
                                Required = argument.IsRequired
                            };
                            value.Arguments.Add(arg);
                        }
                    }
 
                    if (value.Prefix == null)
                    {
                        value.Prefix = propertyToAdd.Prefix;
                    }
 
                    propertyToAdd.Values.Add(value);
                }
            }
 
            // build the dependencies and the values for a parameter
            foreach (XamlTypes.Argument argument in baseProperty.Arguments)
            {
                // To refactor into a separate func
                if (propertyToAdd.Arguments == null)
                {
                    propertyToAdd.Arguments = new List<Argument>();
                }
 
                var arg = new Argument
                {
                    Parameter = argument.Property,
                    Separator = argument.Separator,
                    Required = argument.IsRequired
                };
                propertyToAdd.Arguments.Add(arg);
            }
 
            if (argumentDependencyLookup?.ContainsKey(propertyToAdd.Name) == false)
            {
                argumentDependencyLookup.Add(propertyToAdd.Name, propertyToAdd);
            }
 
            // We've read any enumerated values and any dependencies, so we just
            // have to add the property
            propertyList.AddLast(propertyToAdd);
            return true;
        }
 
        /// <summary>
        /// Gets all the attributes assigned in the xml file for this parameter or all of the nested switches for
        /// this parameter group
        /// </summary>
        private static Property ObtainAttributes(XamlTypes.BaseProperty baseProperty, Property parameterGroup)
        {
            Property parameter;
            if (parameterGroup != null)
            {
                parameter = parameterGroup.Clone();
            }
            else
            {
                parameter = new Property();
            }
 
            XamlTypes.BoolProperty boolProperty = baseProperty as XamlTypes.BoolProperty;
            XamlTypes.DynamicEnumProperty dynamicEnumProperty = baseProperty as XamlTypes.DynamicEnumProperty;
            XamlTypes.EnumProperty enumProperty = baseProperty as XamlTypes.EnumProperty;
            XamlTypes.IntProperty intProperty = baseProperty as XamlTypes.IntProperty;
            XamlTypes.StringProperty stringProperty = baseProperty as XamlTypes.StringProperty;
            XamlTypes.StringListProperty stringListProperty = baseProperty as XamlTypes.StringListProperty;
 
            parameter.IncludeInCommandLine = baseProperty.IncludeInCommandLine;
 
            if (baseProperty.Name != null)
            {
                parameter.Name = baseProperty.Name;
            }
 
            if (boolProperty != null && !String.IsNullOrEmpty(boolProperty.ReverseSwitch))
            {
                parameter.Reversible = "true";
            }
 
            // Determine the type for this property.
            if (boolProperty != null)
            {
                parameter.Type = PropertyType.Boolean;
            }
            else if (enumProperty != null)
            {
                parameter.Type = PropertyType.String;
            }
            else if (dynamicEnumProperty != null)
            {
                parameter.Type = PropertyType.String;
            }
            else if (intProperty != null)
            {
                parameter.Type = PropertyType.Integer;
            }
            else if (stringProperty != null)
            {
                parameter.Type = PropertyType.String;
            }
            else if (stringListProperty != null)
            {
                parameter.Type = PropertyType.StringArray;
            }
 
            // We might need to override this type based on the data source, if it specifies a source type of 'Item'.
            if (baseProperty.DataSource != null)
            {
                if (!String.IsNullOrEmpty(baseProperty.DataSource.SourceType))
                {
                    if (baseProperty.DataSource.SourceType.Equals("Item", StringComparison.OrdinalIgnoreCase))
                    {
                        parameter.Type = PropertyType.ItemArray;
                    }
                }
            }
 
            if (intProperty != null)
            {
                parameter.Max = intProperty.MaxValue?.ToString();
                parameter.Min = intProperty.MinValue?.ToString();
            }
 
            if (boolProperty != null)
            {
                parameter.ReverseSwitchName = boolProperty.ReverseSwitch;
            }
 
            if (baseProperty.Switch != null)
            {
                parameter.SwitchName = baseProperty.Switch;
            }
 
            if (stringListProperty != null)
            {
                parameter.Separator = stringListProperty.Separator;
            }
 
            if (baseProperty.Default != null)
            {
                parameter.DefaultValue = baseProperty.Default;
            }
 
            parameter.Required = baseProperty.IsRequired.ToString().ToLower(CultureInfo.InvariantCulture);
 
            if (baseProperty.Category != null)
            {
                parameter.Category = baseProperty.Category;
            }
 
            if (baseProperty.DisplayName != null)
            {
                parameter.DisplayName = baseProperty.DisplayName;
            }
 
            if (baseProperty.Description != null)
            {
                parameter.Description = baseProperty.Description;
            }
 
            if (baseProperty.SwitchPrefix != null)
            {
                parameter.Prefix = baseProperty.SwitchPrefix;
            }
 
            return parameter;
        }
    }
}