-
Notifications
You must be signed in to change notification settings - Fork 401
Reducing allocations in parsing #2001
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
52af22d
105e592
02c0daf
9741080
d6a5026
bc087b6
c38ccc1
d0e99ee
e919201
11d72b0
4a71f26
c0e3594
e353656
b9c4d42
ac725a3
fea7753
a31e1ec
1002343
30671e8
8e096c8
7eefbeb
8e04f50
d6f38a2
4afe5b5
96c8e77
0bb3b51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -23,31 +23,39 @@ public TypoCorrection(int maxLevenshteinDistance) | |||
|
||||
public void ProvideSuggestions(ParseResult result, IConsole console) | ||||
{ | ||||
for (var i = 0; i < result.UnmatchedTokens.Count; i++) | ||||
var unmatchedTokens = result.UnmatchedTokens; | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to store the result, because currently it's always allocating:
ofc I am going to propose a change to this API |
||||
for (var i = 0; i < unmatchedTokens.Count; i++) | ||||
{ | ||||
var token = result.UnmatchedTokens[i]; | ||||
var suggestions = GetPossibleTokens(result.CommandResult.Command, token).ToList(); | ||||
if (suggestions.Count > 0) | ||||
var token = unmatchedTokens[i]; | ||||
|
||||
bool first = true; | ||||
foreach (string suggestion in GetPossibleTokens(result.CommandResult.Command, token)) | ||||
{ | ||||
console.Out.WriteLine(result.CommandResult.LocalizationResources.SuggestionsTokenNotMatched(token)); | ||||
foreach(string suggestion in suggestions) | ||||
if (first) | ||||
{ | ||||
console.Out.WriteLine(suggestion); | ||||
console.Out.WriteLine(result.CommandResult.LocalizationResources.SuggestionsTokenNotMatched(token)); | ||||
first = false; | ||||
} | ||||
|
||||
console.Out.WriteLine(suggestion); | ||||
} | ||||
} | ||||
} | ||||
|
||||
private IEnumerable<string> GetPossibleTokens(Command targetSymbol, string token) | ||||
{ | ||||
if (!targetSymbol.HasOptions && !targetSymbol.HasSubcommands) | ||||
{ | ||||
return Array.Empty<string>(); | ||||
} | ||||
|
||||
IEnumerable<string> possibleMatches = targetSymbol | ||||
.Children | ||||
.OfType<IdentifierSymbol>() | ||||
.Where(x => !x.IsHidden) | ||||
.Where(x => x.Aliases.Count > 0) | ||||
.Select(symbol => | ||||
symbol.Aliases | ||||
.Union(symbol.Aliases) | ||||
.OrderBy(x => GetDistance(token, x)) | ||||
.ThenByDescending(x => GetStartsWithDistance(token, x)) | ||||
.First() | ||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -14,44 +14,41 @@ namespace System.CommandLine | |||||
/// </summary> | ||||||
public class ParseResult | ||||||
{ | ||||||
private readonly List<ParseError> _errors; | ||||||
private readonly IReadOnlyList<ParseError> _errors; | ||||||
private readonly RootCommandResult _rootCommandResult; | ||||||
private readonly IReadOnlyList<Token> _unmatchedTokens; | ||||||
private Dictionary<string, IReadOnlyList<string>>? _directives; | ||||||
private CompletionContext? _completionContext; | ||||||
|
||||||
internal ParseResult( | ||||||
Parser parser, | ||||||
RootCommandResult rootCommandResult, | ||||||
CommandResult commandResult, | ||||||
IReadOnlyDictionary<string, IReadOnlyList<string>> directives, | ||||||
TokenizeResult tokenizeResult, | ||||||
Dictionary<string, IReadOnlyList<string>>? directives, | ||||||
List<Token> tokens, | ||||||
IReadOnlyList<Token>? unmatchedTokens, | ||||||
List<ParseError>? errors, | ||||||
string? commandLineText = null) | ||||||
{ | ||||||
Parser = parser; | ||||||
_rootCommandResult = rootCommandResult; | ||||||
CommandResult = commandResult; | ||||||
Directives = directives; | ||||||
_directives = directives; | ||||||
|
||||||
// skip the root command when populating Tokens property | ||||||
if (tokenizeResult.Tokens.Count > 1) | ||||||
if (tokens.Count > 1) | ||||||
{ | ||||||
var tokens = new Token[tokenizeResult.Tokens.Count - 1]; | ||||||
for (var i = 0; i < tokenizeResult.Tokens.Count - 1; i++) | ||||||
{ | ||||||
var token = tokenizeResult.Tokens[i + 1]; | ||||||
tokens[i] = token; | ||||||
} | ||||||
|
||||||
// Since TokenizeResult.Tokens is not public and not used anywhere after the parsing, | ||||||
// we take advantage of its mutability and remove the root command token | ||||||
// instead of creating a copy of the whole list. | ||||||
tokens.RemoveAt(0); | ||||||
adamsitnik marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
Tokens = tokens; | ||||||
} | ||||||
else | ||||||
{ | ||||||
Tokens = Array.Empty<Token>(); | ||||||
} | ||||||
|
||||||
_errors = errors ?? new List<ParseError>(); | ||||||
CommandLineText = commandLineText; | ||||||
|
||||||
if (unmatchedTokens is null) | ||||||
|
@@ -67,10 +64,12 @@ internal ParseResult( | |||||
for (var i = 0; i < _unmatchedTokens.Count; i++) | ||||||
{ | ||||||
var token = _unmatchedTokens[i]; | ||||||
_errors.Add(new ParseError(parser.Configuration.LocalizationResources.UnrecognizedCommandOrArgument(token.Value), rootCommandResult)); | ||||||
(errors ??= new()).Add(new ParseError(parser.Configuration.LocalizationResources.UnrecognizedCommandOrArgument(token.Value), rootCommandResult)); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
_errors = errors is not null ? errors : Array.Empty<ParseError>(); | ||||||
} | ||||||
|
||||||
internal static ParseResult Empty() => new RootCommand().Parse(Array.Empty<string>()); | ||||||
|
@@ -99,7 +98,7 @@ internal ParseResult( | |||||
/// Gets the directives found while parsing command line input. | ||||||
/// </summary> | ||||||
/// <remarks>If <see cref="CommandLineConfiguration.EnableDirectives"/> is set to <see langword="false"/>, then this collection will be empty.</remarks> | ||||||
public IReadOnlyDictionary<string, IReadOnlyList<string>> Directives { get; } | ||||||
public IReadOnlyDictionary<string, IReadOnlyList<string>> Directives => _directives ??= new (); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in the future we are going to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Static There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that this is a matter of personal preference. We should discuss it with others, choose one policy and enforce it with static code analysis tools. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there an analyzer for that yet? I don't see one listed at https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/csharp-formatting-options#spacing-options. (There is IDE0090 "Simplify There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
|
||||||
/// <summary> | ||||||
/// Gets the tokens identified while parsing command line input. | ||||||
|
@@ -115,7 +114,8 @@ internal ParseResult( | |||||
/// <summary> | ||||||
/// Gets the list of tokens used on the command line that were not matched by the parser. | ||||||
/// </summary> | ||||||
public IReadOnlyList<string> UnmatchedTokens => _unmatchedTokens.Select(t => t.Value).ToArray(); | ||||||
public IReadOnlyList<string> UnmatchedTokens | ||||||
=> _unmatchedTokens.Count == 0 ? Array.Empty<string>() : _unmatchedTokens.Select(t => t.Value).ToArray(); | ||||||
|
||||||
/// <summary> | ||||||
/// Gets the completion context for the parse result. | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
// 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.Diagnostics; | ||
|
||
namespace System.CommandLine.Parsing | ||
{ | ||
internal class CommandArgumentNode : SyntaxNode | ||
|
@@ -10,10 +12,7 @@ public CommandArgumentNode( | |
Argument argument, | ||
CommandNode parent) : base(token, parent) | ||
{ | ||
if (token.Type != TokenType.Argument) | ||
{ | ||
throw new ArgumentException($"Incorrect token type: {token}"); | ||
} | ||
Debug.Assert(token.Type == TokenType.Argument, $"Incorrect token type: {token}"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these types are internal and we control the input, so there is no need to the throw condition. |
||
|
||
Argument = argument; | ||
ParentCommandNode = parent; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For all the
var
s that don't comply with dotnet/runtime guidelines, are you planning on addressing them all at once in the future?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to discuss it with other contributors. And the usage of
is { }
insteadis not null
which drives me crazy ;p