using System; using System.Collections.Generic; using System.Linq; namespace Spectre.Console.Cli { internal static class CommandModelValidator { public static void Validate(CommandModel model, CommandAppSettings settings) { if (model is null) { throw new ArgumentNullException(nameof(model)); } if (settings is null) { throw new ArgumentNullException(nameof(settings)); } if (model.Commands.Count == 0 && model.DefaultCommand == null) { throw CommandConfigurationException.NoCommandConfigured(); } foreach (var command in model.Commands) { // Alias collision? foreach (var alias in command.Aliases) { if (model.Commands.Any(x => x.Name.Equals(alias, StringComparison.OrdinalIgnoreCase))) { throw CommandConfigurationException.CommandNameConflict(command, alias); } } } Validate(model.DefaultCommand); foreach (var command in model.Commands) { Validate(command); } if (settings.ValidateExamples) { ValidateExamples(model, settings); } } private static void Validate(CommandInfo? command) { if (command == null) { return; } // Get duplicate options for command. var duplicateOptions = GetDuplicates(command); if (duplicateOptions.Length > 0) { throw CommandConfigurationException.DuplicateOption(command, duplicateOptions); } // No children? if (command.IsBranch && command.Children.Count == 0) { throw CommandConfigurationException.BranchHasNoChildren(command); } var arguments = command.Parameters .OfType() .OrderBy(x => x.Position) .ToList(); // vector arguments? if (arguments.Any(x => x.ParameterKind == ParameterKind.Vector)) { // Multiple vector arguments for command? if (arguments.Count(x => x.ParameterKind == ParameterKind.Vector) > 1) { throw CommandConfigurationException.TooManyVectorArguments(command); } // Make sure that the vector argument is specified last. if (arguments.Last().ParameterKind != ParameterKind.Vector) { throw CommandConfigurationException.VectorArgumentNotSpecifiedLast(command); } } // Arguments foreach (var argument in arguments) { if (argument.Required && argument.DefaultValue != null) { throw CommandConfigurationException.RequiredArgumentsCannotHaveDefaultValue(argument); } } // Options var options = command.Parameters.OfType(); foreach (var option in options) { // Pair deconstructable? if (option.Property.PropertyType.IsPairDeconstructable()) { if (option.PairDeconstructor != null && option.Converter != null) { throw CommandConfigurationException.OptionBothHasPairDeconstructorAndTypeParameter(option); } } else if (option.PairDeconstructor != null) { throw CommandConfigurationException.OptionTypeDoesNotSupportDeconstruction(option); } // Optional options that are not flags? if (option.ParameterKind == ParameterKind.FlagWithValue && !option.IsFlagValue()) { throw CommandConfigurationException.OptionalOptionValueMustBeFlagWithValue(option); } } // Validate child commands. foreach (var childCommand in command.Children) { Validate(childCommand); } } private static void ValidateExamples(CommandModel model, CommandAppSettings settings) { var examples = new List(); examples.AddRangeIfNotNull(model.Examples); // Get all examples. var queue = new Queue(new[] { model }); while (queue.Count > 0) { var current = queue.Dequeue(); foreach (var command in current.Commands) { examples.AddRangeIfNotNull(command.Examples); queue.Enqueue(command); } } // Validate all examples. foreach (var example in examples) { try { var parser = new CommandTreeParser(model, settings, ParsingMode.Strict); parser.Parse(example); } catch (Exception ex) { throw new CommandConfigurationException("Validation of examples failed.", ex); } } } private static string[] GetDuplicates(CommandInfo command) { var result = new Dictionary(StringComparer.Ordinal); void AddToResult(IEnumerable keys) { foreach (var key in keys) { if (!string.IsNullOrWhiteSpace(key)) { if (!result.ContainsKey(key)) { result.Add(key, 0); } result[key]++; } } } foreach (var option in command.Parameters.OfType()) { AddToResult(option.ShortNames); AddToResult(option.LongNames); } return result.Where(x => x.Value > 1) .Select(x => x.Key).ToArray(); } } }