diff --git a/Directory.Packages.props b/Directory.Packages.props index a8a61f0901..e31ef9f32c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,12 +1,10 @@ - true false $(NoWarn);NU1507 - @@ -24,14 +22,13 @@ - + + - - - + \ No newline at end of file diff --git a/src/System.CommandLine/ParseResult.cs b/src/System.CommandLine/ParseResult.cs index c0001edb14..3a7e1b7b86 100644 --- a/src/System.CommandLine/ParseResult.cs +++ b/src/System.CommandLine/ParseResult.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; @@ -36,7 +36,7 @@ internal ParseResult( */ // TODO: unmatched tokens // List? unmatchedTokens, - List? errors, + List? errors, // TODO: commandLineText should be string array string? commandLineText = null //, // TODO: invocation @@ -75,8 +75,8 @@ internal ParseResult( // TODO: unmatched tokens // _unmatchedTokens = unmatchedTokens is null ? Array.Empty() : unmatchedTokens; - - Errors = errors is not null ? errors : Array.Empty(); + + Errors = errors is not null ? errors : Array.Empty(); } // TODO: check that constructing empty ParseResult directly is correct @@ -102,7 +102,7 @@ internal ParseResult( /// /// Gets the parse errors found while parsing command line input. /// - public IReadOnlyList Errors { get; } + public IReadOnlyList Errors { get; } /* // TODO: don't expose tokens diff --git a/src/System.CommandLine/Parsing/ArgumentResult.cs b/src/System.CommandLine/Parsing/ArgumentResult.cs index 1a7c6e4cd7..bd1f56c717 100644 --- a/src/System.CommandLine/Parsing/ArgumentResult.cs +++ b/src/System.CommandLine/Parsing/ArgumentResult.cs @@ -141,7 +141,7 @@ public void OnlyTake(int numberOfTokens) /// internal override void AddError(string errorMessage) { - SymbolResultTree.AddError(new ParseError(errorMessage, AppliesToPublicSymbolResult)); + SymbolResultTree.AddError(new CliDiagnostic(new("", "", errorMessage, CliDiagnosticSeverity.Warning, null), [], symbolResult: AppliesToPublicSymbolResult)); _conversionResult = ArgumentConversionResult.Failure(this, errorMessage, ArgumentConversionResultType.Failed); } @@ -171,9 +171,6 @@ private ArgumentConversionResult ValidateAndConvert(bool useValidators) } } */ - - // TODO: defaults - /* if (Parent!.UseDefaultValueFor(this)) { var defaultValue = Argument.GetDefaultValue(this); @@ -181,7 +178,6 @@ private ArgumentConversionResult ValidateAndConvert(bool useValidators) // default value factory provided by the user might report an error, which sets _conversionResult return _conversionResult ?? ArgumentConversionResult.Success(this, defaultValue); } - */ if (Argument.ConvertArguments is null) { @@ -223,7 +219,7 @@ ArgumentConversionResult ReportErrorIfNeeded(ArgumentConversionResult result) { if (result.Result >= ArgumentConversionResultType.Failed) { - SymbolResultTree.AddError(new ParseError(result.ErrorMessage!, AppliesToPublicSymbolResult)); + SymbolResultTree.AddError(new CliDiagnostic(new("ArgumentConversionResultTypeFailed", "Type Conversion Failed", result.ErrorMessage!, CliDiagnosticSeverity.Warning, null), [], symbolResult: AppliesToPublicSymbolResult)); } return result; diff --git a/src/System.CommandLine/Parsing/CliDiagnostic.cs b/src/System.CommandLine/Parsing/CliDiagnostic.cs new file mode 100644 index 0000000000..8328683f7b --- /dev/null +++ b/src/System.CommandLine/Parsing/CliDiagnostic.cs @@ -0,0 +1,111 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +namespace System.CommandLine.Parsing; + +/* + * Pattern based on: + * https://github.com/mhutch/MonoDevelop.MSBuildEditor/blob/main/MonoDevelop.MSBuild/Analysis/MSBuildDiagnostic.cs + * https://github.com/mhutch/MonoDevelop.MSBuildEditor/blob/main/MonoDevelop.MSBuild/Analysis/MSBuildDiagnosticDescriptor.cs + * https://github.com/dotnet/roslyn/blob/main/src/Compilers/Core/Portable/Diagnostic/DiagnosticDescriptor.cs + * https://github.com/dotnet/roslyn/blob/main/src/Compilers/Core/Portable/Diagnostic/Diagnostic.cs + */ +internal static class ParseDiagnostics +{ + public const string DirectiveIsNotDefinedId = "CMD0001"; + public static readonly CliDiagnosticDescriptor DirectiveIsNotDefined = + new( + DirectiveIsNotDefinedId, + //TODO: use localized strings + "Directive is not defined", + "The directive '{0}' is not defined.", + CliDiagnosticSeverity.Error, + null); +} + +public sealed class CliDiagnosticDescriptor +{ + public CliDiagnosticDescriptor(string id, string title, string messageFormat, CliDiagnosticSeverity severity, string? helpUri) + { + Id = id; + Title = title; + MessageFormat = messageFormat; + Severity = severity; + HelpUri = helpUri; + } + + public string Id { get; } + public string Title { get; } + public string MessageFormat { get; } + public CliDiagnosticSeverity Severity { get; } + public string? HelpUri { get; } +} + +public enum CliDiagnosticSeverity +{ + Hidden = 0, + Info, + Warning, + Error +} + +/// +/// Describes an error that occurs while parsing command line input. +/// +public sealed class CliDiagnostic +{ + // TODO: reevaluate whether we should be exposing a SymbolResult here + // TODO: Rename to CliError + + /// + /// Initializes a new instance of the class. + /// + /// Contains information about the error. + /// The arguments to be passed to the in the . + /// Properties to be associated with the diagnostic. + /// The symbol result detailing the symbol that failed to parse and the tokens involved. + /// The location of the error. + public CliDiagnostic( + CliDiagnosticDescriptor descriptor, + object?[]? messageArgs, + ImmutableDictionary? properties = null, + SymbolResult? symbolResult = null, + Location? location = null) + { + Descriptor = descriptor; + MessageArgs = messageArgs; + Properties = properties; + SymbolResult = symbolResult; + } + + /// + /// Gets a message to explain the error to a user. + /// + public string Message + { + get + { + if (MessageArgs is not null) + { + return string.Format(Descriptor.MessageFormat, MessageArgs); + } + return Descriptor.MessageFormat; + } + } + + public ImmutableDictionary? Properties { get; } + + public CliDiagnosticDescriptor Descriptor { get; } + + public object?[]? MessageArgs { get; } + + /// + /// Gets the symbol result detailing the symbol that failed to parse and the tokens involved. + /// + public SymbolResult? SymbolResult { get; } + + /// + public override string ToString() => Message; +} diff --git a/src/System.CommandLine/Parsing/CommandResult.cs b/src/System.CommandLine/Parsing/CommandResult.cs index 7adf32d08f..f654bac87d 100644 --- a/src/System.CommandLine/Parsing/CommandResult.cs +++ b/src/System.CommandLine/Parsing/CommandResult.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; @@ -64,7 +64,7 @@ internal void Validate(bool completeValidation) if (Command.HasSubcommands) { SymbolResultTree.InsertFirstError( - new ParseError(LocalizationResources.RequiredCommandWasNotProvided(), this)); + new CliDiagnostic(new("validateSubCommandError", "Validation Error", LocalizationResources.RequiredCommandWasNotProvided(), CliDiagnosticSeverity.Warning, null), [], symbolResult: this)); } // TODO: validators diff --git a/src/System.CommandLine/Parsing/Location.cs b/src/System.CommandLine/Parsing/Location.cs index 18d97ce681..00be871373 100644 --- a/src/System.CommandLine/Parsing/Location.cs +++ b/src/System.CommandLine/Parsing/Location.cs @@ -49,6 +49,7 @@ public Location(string text, string source, int start, Location? outerLocation, public bool IsImplicit => Source == Implicit; + /// public override string ToString() => $"{(OuterLocation is null ? "" : OuterLocation.ToString() + "; ")}{Text} from {Source}[{Start}, {Length}, {Offset}]"; diff --git a/src/System.CommandLine/Parsing/ParseError.cs b/src/System.CommandLine/Parsing/ParseError.cs deleted file mode 100644 index 5c5453a48e..0000000000 --- a/src/System.CommandLine/Parsing/ParseError.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace System.CommandLine.Parsing -{ - /// - /// Describes an error that occurs while parsing command line input. - /// - public sealed class ParseError - { - // TODO: add position - // TODO: reevaluate whether we should be exposing a SymbolResult here - internal ParseError( - string message, - SymbolResult? symbolResult = null) - { - if (string.IsNullOrWhiteSpace(message)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(message)); - } - - Message = message; - SymbolResult = symbolResult; - } - - /// - /// A message to explain the error to a user. - /// - public string Message { get; } - - /// - /// The symbol result detailing the symbol that failed to parse and the tokens involved. - /// - public SymbolResult? SymbolResult { get; } - - /// - public override string ToString() => Message; - } -} diff --git a/src/System.CommandLine/Parsing/SymbolResult.cs b/src/System.CommandLine/Parsing/SymbolResult.cs index 25b1c17b08..d716e14870 100644 --- a/src/System.CommandLine/Parsing/SymbolResult.cs +++ b/src/System.CommandLine/Parsing/SymbolResult.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; @@ -24,7 +24,7 @@ private protected SymbolResult(SymbolResultTree symbolResultTree, SymbolResult? /// /// The parse errors associated with this symbol result. /// - public IEnumerable Errors + public IEnumerable Errors { get { @@ -64,7 +64,7 @@ public IEnumerable Errors /// Adds an error message for this symbol result to it's parse tree. /// /// Setting an error will cause the parser to indicate an error for the user and prevent invocation of the command line. - internal virtual void AddError(string errorMessage) => SymbolResultTree.AddError(new ParseError(errorMessage, this)); + internal virtual void AddError(string errorMessage) => SymbolResultTree.AddError(new CliDiagnostic(new("", "", errorMessage, severity: CliDiagnosticSeverity.Error, null), [], symbolResult: this)); /// /// Finds a result for the specific argument anywhere in the parse tree, including parent and child symbol results. /// @@ -100,7 +100,7 @@ public IEnumerable Errors /// /// The name of the symbol for which to find a result. /// An argument result if the argument was matched by the parser or has a default value; otherwise, null. - public SymbolResult? GetResult(string name) => + public SymbolResult? GetResult(string name) => SymbolResultTree.GetResult(name); /// diff --git a/src/System.CommandLine/Parsing/SymbolResultTree.cs b/src/System.CommandLine/Parsing/SymbolResultTree.cs index 37319d604a..abfaa30953 100644 --- a/src/System.CommandLine/Parsing/SymbolResultTree.cs +++ b/src/System.CommandLine/Parsing/SymbolResultTree.cs @@ -1,15 +1,14 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; -using System.Linq; namespace System.CommandLine.Parsing { internal sealed class SymbolResultTree : Dictionary { private readonly CliCommand _rootCommand; - internal List? Errors; + internal List? Errors; // TODO: unmatched tokens /* internal List? UnmatchedTokens; @@ -27,11 +26,11 @@ internal SymbolResultTree( if (tokenizeErrors is not null) { - Errors = new List(tokenizeErrors.Count); + Errors = new List(tokenizeErrors.Count); for (var i = 0; i < tokenizeErrors.Count; i++) { - Errors.Add(new ParseError(tokenizeErrors[i])); + Errors.Add(new CliDiagnostic(new("", "", tokenizeErrors[i], CliDiagnosticSeverity.Warning, null), [])); } } } @@ -51,7 +50,6 @@ internal SymbolResultTree( internal DirectiveResult? GetResult(CliDirective directive) => TryGetValue(directive, out SymbolResult? result) ? (DirectiveResult)result : default; */ - // TODO: Determine how this is used. It appears to be O^n in the size of the tree and so if it is called multiple times, we should reconsider to avoid O^(N*M) internal IEnumerable GetChildren(SymbolResult parent) { // Argument can't have children @@ -71,7 +69,7 @@ internal Dictionary GetValueResultDictionary() { var dict = new Dictionary(); foreach (KeyValuePair pair in this) - { + { var result = pair.Value; if (result is OptionResult optionResult) { @@ -87,8 +85,8 @@ internal Dictionary GetValueResultDictionary() return dict; } - internal void AddError(ParseError parseError) => (Errors ??= new()).Add(parseError); - internal void InsertFirstError(ParseError parseError) => (Errors ??= new()).Insert(0, parseError); + internal void AddError(CliDiagnostic parseError) => (Errors ??= new()).Add(parseError); + internal void InsertFirstError(CliDiagnostic parseError) => (Errors ??= new()).Insert(0, parseError); internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, CommandResult rootCommandResult) { @@ -104,7 +102,7 @@ internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, Com } */ - AddError(new ParseError(LocalizationResources.UnrecognizedCommandOrArgument(token.Value), commandResult)); + AddError(new CliDiagnostic(new("", "", LocalizationResources.UnrecognizedCommandOrArgument(token.Value), CliDiagnosticSeverity.Warning, null), [], symbolResult: commandResult)); // } } @@ -113,7 +111,6 @@ internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, Com if (_symbolsByName is null) { _symbolsByName = new(); - // TODO: See if we can avoid populating the entire tree and just populate the portion/cone we need PopulateSymbolsByName(_rootCommand); } @@ -140,7 +137,6 @@ internal void AddUnmatchedToken(CliToken token, CommandResult commandResult, Com // so we could avoid using their value factories and adding them to the dictionary // could we sort by name allowing us to do a binary search instead of allocating a dictionary? // could we add codepaths that query for specific kinds of symbols so they don't have to search all symbols? -// Additional Note: Couldn't commands know their children, and thus this involves querying the active command, and possibly the parents private void PopulateSymbolsByName(CliCommand command) { if (command.HasArguments) diff --git a/src/System.CommandLine/System.CommandLine.csproj b/src/System.CommandLine/System.CommandLine.csproj index 6cd32ef4d8..f394e92420 100644 --- a/src/System.CommandLine/System.CommandLine.csproj +++ b/src/System.CommandLine/System.CommandLine.csproj @@ -55,7 +55,7 @@ - + @@ -72,6 +72,7 @@ +