Do not try the same (command, wraps) pair more than once when completing

This prevents runaway wrap chains. Fixes #5638.
This commit is contained in:
ridiculousfish 2019-09-21 13:38:59 -07:00
parent 77dbe109e0
commit 5f64972908
2 changed files with 57 additions and 11 deletions

View File

@ -19,6 +19,7 @@
#include <list>
#include <memory>
#include <numeric>
#include <set>
#include <string>
#include <type_traits>
#include <unordered_map>
@ -1307,16 +1308,16 @@ bool completer_t::try_complete_user(const wcstring &str) {
#endif
}
// The callback type for walk_wrap_chain
// The callback type for walk_wrap_chain.
using wrap_chain_visitor_t = std::function<void(const wcstring &, const wcstring &, size_t depth)>;
// Helper to complete a parameter for a command and its transitive wrap chain.
// Given a command line \p command_line and the range of the command itself within the command line
// as \p command_range, invoke the \p receiver with the command and the command line. Then, for each
// target wrapped by the given command, update the command line with that target and invoke this
// recursively.
static void walk_wrap_chain(const wcstring &command_line, source_range_t command_range,
const wrap_chain_visitor_t &visitor, size_t depth = 0) {
// A set tracking which (command, wrap) pairs we have seen.
using wrap_chain_visited_set_t = std::set<std::pair<wcstring, wcstring>>;
// Recursive implementation of walk_wrap_chain().
static void walk_wrap_chain_recursive(const wcstring &command_line, source_range_t command_range,
const wrap_chain_visitor_t &visitor,
wrap_chain_visited_set_t *visited, size_t depth) {
// Limit our recursion depth. This prevents cycles in the wrap chain graph from overflowing.
if (depth > 24) return;
if (reader_test_should_cancel()) return;
@ -1339,14 +1340,30 @@ static void walk_wrap_chain(const wcstring &command_line, source_range_t command
if (!wrapped_command.empty()) {
size_t where = faux_commandline.find(wrapped_command, command_range.start);
if (where != wcstring::npos) {
// Recurse with our new command and command line.
source_range_t faux_source_range{uint32_t(where), uint32_t(wrapped_command.size())};
walk_wrap_chain(faux_commandline, faux_source_range, visitor, depth + 1);
// Do not recurse if we have already seen this.
if (visited->insert({command, wrapped_command}).second) {
// Recurse with our new command and command line.
source_range_t faux_source_range{uint32_t(where),
uint32_t(wrapped_command.size())};
walk_wrap_chain_recursive(faux_commandline, faux_source_range, visitor, visited,
depth + 1);
}
}
}
}
}
// Helper to complete a parameter for a command and its transitive wrap chain.
// Given a command line \p command_line and the range of the command itself within the command line
// as \p command_range, invoke the \p receiver with the command and the command line. Then, for each
// target wrapped by the given command, update the command line with that target and invoke this
// recursively.
static void walk_wrap_chain(const wcstring &command_line, source_range_t command_range,
const wrap_chain_visitor_t &visitor) {
wrap_chain_visited_set_t visited;
walk_wrap_chain_recursive(command_line, command_range, visitor, &visited, 0);
}
/// If the argument contains a '[' typed by the user, completion by appending to the argument might
/// produce an invalid token (#5831).
///

29
tests/checks/wraps.fish Normal file
View File

@ -0,0 +1,29 @@
#RUN: %fish %s
# Validate some things about command wrapping.
# This tests that we do not trigger a combinatorial explosion - see #5638.
# Ensure it completes successully.
complete -c testcommand --wraps "testcommand x "
complete -c testcommand --wraps "testcommand y "
complete -c testcommand --no-files -a normal
complete -C'testcommand '
# CHECK: normal
# We get the same completion twice. TODO: fix this.
# CHECK: normal
# This tests that a call to complete from within a completion doesn't trigger
# wrap chain explosion - #5638 again.
function testcommand2_complete
set -l tokens (commandline -opc) (commandline -ct)
set -e tokens[1]
echo $tokens 1>&2
complete -C"$tokens"
end
complete -c testcommand2 -x -a "(testcommand2_complete)"
complete -c testcommand2 --wraps "testcommand2 from_wraps "
complete -C'testcommand2 explicit '
# CHECKERR: explicit
# CHECKERR: from_wraps explicit