Initial work on backtrace support with new parser

This commit is contained in:
ridiculousfish 2013-12-15 16:05:37 -08:00
parent 5cf59de676
commit 5b24aac266
7 changed files with 237 additions and 190 deletions

View File

@ -499,7 +499,7 @@ static int builtin_complete(parser_t &parser, wchar_t **argv)
{ {
const wcstring condition_string = condition; const wcstring condition_string = condition;
parse_error_list_t errors; parse_error_list_t errors;
if (parser.detect_errors(condition_string, &errors)) if (parse_util_detect_errors(condition_string, &errors))
{ {
append_format(stderr_buffer, append_format(stderr_buffer,
L"%ls: Condition '%ls' contained a syntax error", L"%ls: Condition '%ls' contained a syntax error",

View File

@ -119,6 +119,13 @@ enum parse_error_code_t
parse_error_unbalancing_case, //case outside of switch parse_error_unbalancing_case, //case outside of switch
}; };
enum {
PARSER_TEST_ERROR = 1,
PARSER_TEST_INCOMPLETE = 2
};
typedef unsigned int parser_test_error_bits_t;
/** /**
Error message for tokenizer error. The tokenizer message is Error message for tokenizer error. The tokenizer message is

View File

@ -39,18 +39,12 @@
#include "signal.h" #include "signal.h"
#include "wildcard.h" #include "wildcard.h"
#include "parse_tree.h" #include "parse_tree.h"
#include "parser.h"
/** /**
Maximum number of autoloaded items opf a specific type to keep in Error message for improper use of the exec builtin
memory at a time.
*/ */
#define AUTOLOAD_MAX 10 #define EXEC_ERR_MSG _(L"This command can not be used in a pipeline")
/**
Minimum time, in seconds, before an autoloaded item will be
unloaded
*/
#define AUTOLOAD_MIN_AGE 60
int parse_util_lineno(const wchar_t *str, size_t offset) int parse_util_lineno(const wchar_t *str, size_t offset)
{ {
@ -940,3 +934,192 @@ std::vector<int> parse_util_compute_indents(const wcstring &src)
return indents; return indents;
} }
/* Append a syntax error to the given error list */
static bool append_syntax_error(parse_error_list_t *errors, const parse_node_t &node, const wchar_t *fmt, ...)
{
parse_error_t error;
error.source_start = node.source_start;
error.source_length = node.source_length;
error.code = parse_error_syntax;
va_list va;
va_start(va, fmt);
error.text = vformat_string(fmt, va);
va_end(va);
errors->push_back(error);
return true;
}
/**
Returns 1 if the specified command is a builtin that may not be used in a pipeline
*/
static int parser_is_pipe_forbidden(const wcstring &word)
{
return contains(word,
L"exec",
L"case",
L"break",
L"return",
L"continue");
}
// Check if the first argument under the given node is --help
static bool first_argument_is_help(const parse_node_tree_t &node_tree, const parse_node_t &node, const wcstring &src)
{
bool is_help = false;
const parse_node_tree_t::parse_node_list_t arg_nodes = node_tree.find_nodes(node, symbol_argument, 1);
if (! arg_nodes.empty())
{
// Check the first argument only
const parse_node_t &arg = *arg_nodes.at(0);
const wcstring first_arg_src = arg.get_source(src);
is_help = parser_t::is_help(first_arg_src.c_str(), 3);
}
return is_help;
}
parser_test_error_bits_t parse_util_detect_errors(const wcstring &buff_src, parse_error_list_t *out_errors)
{
parse_node_tree_t node_tree;
parse_error_list_t parse_errors;
// Whether we encountered a parse error
bool errored = false;
// Whether we encountered an unclosed block
// We detect this via an 'end_command' block without source
bool has_unclosed_block = false;
// Parse the input string into a parse tree
// Some errors are detected here
bool parsed = parse_t::parse(buff_src, 0, &node_tree, &parse_errors);
if (! parsed)
{
errored = true;
}
// Expand all commands
// Verify 'or' and 'and' not used inside pipelines
// Verify pipes via parser_is_pipe_forbidden
// Verify return only within a function
if (! errored)
{
const size_t node_tree_size = node_tree.size();
for (size_t i=0; i < node_tree_size; i++)
{
const parse_node_t &node = node_tree.at(i);
if (node.type == symbol_end_command && ! node.has_source())
{
// an 'end' without source is an unclosed block
has_unclosed_block = true;
}
else if (node.type == symbol_plain_statement)
{
wcstring command;
if (node_tree.command_for_plain_statement(node, buff_src, &command))
{
// Check that we can expand the command
if (! expand_one(command, EXPAND_SKIP_CMDSUBST | EXPAND_SKIP_VARIABLES | EXPAND_SKIP_JOBS))
{
errored = append_syntax_error(&parse_errors, node, ILLEGAL_CMD_ERR_MSG, command.c_str());
}
// Check that pipes are sound
bool is_boolean_command = contains(command, L"or", L"and");
bool is_pipe_forbidden = parser_is_pipe_forbidden(command);
if (! errored && (is_boolean_command || is_pipe_forbidden))
{
// 'or' and 'and' can be first in the pipeline. forbidden commands cannot be in a pipeline at all
if (node_tree.plain_statement_is_in_pipeline(node, is_pipe_forbidden))
{
errored = append_syntax_error(&parse_errors, node, EXEC_ERR_MSG);
}
}
// Check that we don't return from outside a function
// But we allow it if it's 'return --help'
if (! errored && command == L"return")
{
const parse_node_t *ancestor = &node;
bool found_function = false;
while (ancestor != NULL)
{
const parse_node_t *possible_function_header = node_tree.header_node_for_block_statement(*ancestor);
if (possible_function_header != NULL && possible_function_header->type == symbol_function_header)
{
found_function = true;
break;
}
ancestor = node_tree.get_parent(*ancestor);
}
if (! found_function && ! first_argument_is_help(node_tree, node, buff_src))
{
errored = append_syntax_error(&parse_errors, node, INVALID_RETURN_ERR_MSG);
}
}
// Check that we don't return from outside a function
if (! errored && (command == L"break" || command == L"continue"))
{
// Walk up until we hit a 'for' or 'while' loop. If we hit a function first, stop the search; we can't break an outer loop from inside a function.
// This is a little funny because we can't tell if it's a 'for' or 'while' loop from the ancestor alone; we need the header. That is, we hit a block_statement, and have to check its header.
bool found_loop = false, end_search = false;
const parse_node_t *ancestor = &node;
while (ancestor != NULL && ! end_search)
{
const parse_node_t *loop_or_function_header = node_tree.header_node_for_block_statement(*ancestor);
if (loop_or_function_header != NULL)
{
switch (loop_or_function_header->type)
{
case symbol_while_header:
case symbol_for_header:
// this is a loop header, so we can break or continue
found_loop = true;
end_search = true;
break;
case symbol_function_header:
// this is a function header, so we cannot break or continue. We stop our search here.
found_loop = false;
end_search = true;
break;
default:
// most likely begin / end style block, which makes no difference
break;
}
}
ancestor = node_tree.get_parent(*ancestor);
}
if (! found_loop && ! first_argument_is_help(node_tree, node, buff_src))
{
errored = append_syntax_error(&parse_errors, node, INVALID_LOOP_ERR_MSG);
}
}
}
}
}
}
parser_test_error_bits_t res = 0;
if (errored)
res |= PARSER_TEST_ERROR;
if (has_unclosed_block)
res |= PARSER_TEST_INCOMPLETE;
if (out_errors)
{
out_errors->swap(parse_errors);
}
return res;
}

View File

@ -8,6 +8,7 @@
#define FISH_PARSE_UTIL_H #define FISH_PARSE_UTIL_H
#include "autoload.h" #include "autoload.h"
#include "parse_tree.h"
#include <wchar.h> #include <wchar.h>
#include <map> #include <map>
#include <set> #include <set>
@ -162,4 +163,6 @@ wcstring parse_util_escape_string_with_quote(const wcstring &cmd, wchar_t quote)
/** Given a string, parse it as fish code and then return the indents. The return value has the same size as the string */ /** Given a string, parse it as fish code and then return the indents. The return value has the same size as the string */
std::vector<int> parse_util_compute_indents(const wcstring &src); std::vector<int> parse_util_compute_indents(const wcstring &src);
parser_test_error_bits_t parse_util_detect_errors(const wcstring &buff_src, parse_error_list_t *out_errors);
#endif #endif

View File

@ -801,7 +801,7 @@ void parser_t::eval_args(const wchar_t *line, std::vector<completion_t> &args)
proc_pop_interactive(); proc_pop_interactive();
} }
void parser_t::stack_trace(block_t *b, wcstring &buff) void parser_t::stack_trace(block_t *b, wcstring &buff) const
{ {
/* /*
Check if we should end the recursion Check if we should end the recursion
@ -844,7 +844,7 @@ void parser_t::stack_trace(block_t *b, wcstring &buff)
{ {
const source_block_t *sb = static_cast<const source_block_t*>(b); const source_block_t *sb = static_cast<const source_block_t*>(b);
const wchar_t *source_dest = sb->source_file; const wchar_t *source_dest = sb->source_file;
append_format(buff, _(L"in . (source) call of file '%ls',\n"), source_dest); append_format(buff, _(L"from sourcing file '%ls',\n"), source_dest);
break; break;
} }
case FUNCTION_CALL: case FUNCTION_CALL:
@ -2762,7 +2762,7 @@ int parser_t::parser_test_argument(const wchar_t *arg, wcstring *out, const wcha
// debug( 1, L"%ls -> %ls %ls", arg_cpy, subst, tmp.buff ); // debug( 1, L"%ls -> %ls %ls", arg_cpy, subst, tmp.buff );
parse_error_list_t errors; parse_error_list_t errors;
err |= parser_t::detect_errors(subst, &errors); err |= parse_util_detect_errors(subst, &errors);
if (out && ! errors.empty()) if (out && ! errors.empty())
{ {
out->append(parse_errors_description(errors, subst, prefix)); out->append(parse_errors_description(errors, subst, prefix));
@ -2890,21 +2890,6 @@ int parser_t::test_args(const wchar_t * buff, wcstring *out, const wchar_t *pre
return err; return err;
} }
// Check if the first argument under the given node is --help
static bool first_argument_is_help(const parse_node_tree_t &node_tree, const parse_node_t &node, const wcstring &src)
{
bool is_help = false;
const parse_node_tree_t::parse_node_list_t arg_nodes = node_tree.find_nodes(node, symbol_argument, 1);
if (! arg_nodes.empty())
{
// Check the first argument only
const parse_node_t &arg = *arg_nodes.at(0);
const wcstring first_arg_src = arg.get_source(src);
is_help = parser_t::is_help(first_arg_src.c_str(), 3);
}
return is_help;
}
// helper type used in parser::test below // helper type used in parser::test below
struct block_info_t struct block_info_t
{ {
@ -2929,153 +2914,31 @@ static bool append_syntax_error(parse_error_list_t *errors, const parse_node_t &
return true; return true;
} }
parser_test_error_bits_t parser_t::detect_errors(const wcstring &buff_src, parse_error_list_t *out_errors, const wchar_t *prefix) void parser_t::get_backtrace(const wcstring &src, const parse_error_list_t &errors, wcstring *output) const
{ {
ASSERT_IS_MAIN_THREAD(); assert(output != NULL);
if (! errors.empty())
parse_node_tree_t node_tree;
parse_error_list_t parse_errors;
// Whether we encountered a parse error
bool errored = false;
// Whether we encountered an unclosed block
// We detect this via an 'end_command' block without source
bool has_unclosed_block = false;
// Parse the input string into a parse tree
// Some errors are detected here
bool parsed = parse_t::parse(buff_src, 0, &node_tree, &parse_errors);
if (! parsed)
{ {
errored = true; const parse_error_t err = errors.at(0);
} output->append(err.describe(src));
output->push_back(L'\n');
// Expand all commands
// Verify 'or' and 'and' not used inside pipelines // Determine which line we're on
// Verify pipes via parser_is_pipe_forbidden assert(err.source_start <= src.size());
// Verify return only within a function size_t which_line = 1 + std::count(src.begin(), src.begin() + err.source_start, L'\n');
if (! errored) const wchar_t *filename = this->current_filename();
{ if (filename)
const size_t node_tree_size = node_tree.size();
for (size_t i=0; i < node_tree_size; i++)
{ {
const parse_node_t &node = node_tree.at(i); append_format(*output, _(L"line %lu of '%ls'\n"), which_line, filename);
if (node.type == symbol_end_command && ! node.has_source())
{
// an 'end' without source is an unclosed block
has_unclosed_block = true;
}
else if (node.type == symbol_plain_statement)
{
wcstring command;
if (node_tree.command_for_plain_statement(node, buff_src, &command))
{
// Check that we can expand the command
if (! expand_one(command, EXPAND_SKIP_CMDSUBST | EXPAND_SKIP_VARIABLES | EXPAND_SKIP_JOBS))
{
errored = append_syntax_error(&parse_errors, node, ILLEGAL_CMD_ERR_MSG, command.c_str());
}
// Check that pipes are sound
bool is_boolean_command = contains(command, L"or", L"and");
bool is_pipe_forbidden = parser_is_pipe_forbidden(command);
if (! errored && (is_boolean_command || is_pipe_forbidden))
{
// 'or' and 'and' can be first in the pipeline. forbidden commands cannot be in a pipeline at all
if (node_tree.plain_statement_is_in_pipeline(node, is_pipe_forbidden))
{
errored = append_syntax_error(&parse_errors, node, EXEC_ERR_MSG);
}
}
// Check that we don't return from outside a function
// But we allow it if it's 'return --help'
if (! errored && command == L"return")
{
const parse_node_t *ancestor = &node;
bool found_function = false;
while (ancestor != NULL)
{
const parse_node_t *possible_function_header = node_tree.header_node_for_block_statement(*ancestor);
if (possible_function_header != NULL && possible_function_header->type == symbol_function_header)
{
found_function = true;
break;
}
ancestor = node_tree.get_parent(*ancestor);
}
if (! found_function && ! first_argument_is_help(node_tree, node, buff_src))
{
errored = append_syntax_error(&parse_errors, node, INVALID_RETURN_ERR_MSG);
}
}
// Check that we don't return from outside a function
if (! errored && (command == L"break" || command == L"continue"))
{
// Walk up until we hit a 'for' or 'while' loop. If we hit a function first, stop the search; we can't break an outer loop from inside a function.
// This is a little funny because we can't tell if it's a 'for' or 'while' loop from the ancestor alone; we need the header. That is, we hit a block_statement, and have to check its header.
bool found_loop = false, end_search = false;
const parse_node_t *ancestor = &node;
while (ancestor != NULL && ! end_search)
{
const parse_node_t *loop_or_function_header = node_tree.header_node_for_block_statement(*ancestor);
if (loop_or_function_header != NULL)
{
switch (loop_or_function_header->type)
{
case symbol_while_header:
case symbol_for_header:
// this is a loop header, so we can break or continue
found_loop = true;
end_search = true;
break;
case symbol_function_header:
// this is a function header, so we cannot break or continue. We stop our search here.
found_loop = false;
end_search = true;
break;
default:
// most likely begin / end style block, which makes no difference
break;
}
}
ancestor = node_tree.get_parent(*ancestor);
}
if (! found_loop && ! first_argument_is_help(node_tree, node, buff_src))
{
errored = append_syntax_error(&parse_errors, node, INVALID_LOOP_ERR_MSG);
}
}
}
}
} }
else
{
append_format(*output, L"%ls: ", _(L"Standard input"), which_line);
}
this->stack_trace(current_block, *output);
} }
parser_test_error_bits_t res = 0;
if (errored)
res |= PARSER_TEST_ERROR;
if (has_unclosed_block)
res |= PARSER_TEST_INCOMPLETE;
if (out_errors)
{
out_errors->swap(parse_errors);
}
error_code=0;
return res;
} }
parser_test_error_bits_t parser_t::detect_errors2(const wchar_t *buff, wcstring *out, const wchar_t *prefix) parser_test_error_bits_t parser_t::detect_errors2(const wchar_t *buff, wcstring *out, const wchar_t *prefix)

View File

@ -14,12 +14,6 @@
#include "parse_tree.h" #include "parse_tree.h"
#include <vector> #include <vector>
enum {
PARSER_TEST_ERROR = 1,
PARSER_TEST_INCOMPLETE = 2
};
typedef unsigned int parser_test_error_bits_t;
/** /**
event_blockage_t represents a block on events of the specified type event_blockage_t represents a block on events of the specified type
*/ */
@ -488,8 +482,8 @@ public:
\param out if non-null, any errors in the command will be filled out into this buffer \param out if non-null, any errors in the command will be filled out into this buffer
\param prefix the prefix string to prepend to each error message written to the \c out buffer \param prefix the prefix string to prepend to each error message written to the \c out buffer
*/ */
parser_test_error_bits_t detect_errors(const wcstring &buff, parse_error_list_t *out_errors = NULL, const wchar_t *prefix = NULL); parser_test_error_bits_t detect_errors2(const wchar_t *buff, wcstring *out_error_desc, const wchar_t *prefix);
parser_test_error_bits_t detect_errors2(const wchar_t * buff, wcstring *out = NULL, const wchar_t *prefix = NULL); void get_backtrace(const wcstring &src, const parse_error_list_t &errors, wcstring *output) const;
/** /**
Test if the specified string can be parsed as an argument list, Test if the specified string can be parsed as an argument list,
@ -538,7 +532,7 @@ public:
/** /**
Write a stack trace starting at the specified block to the specified wcstring Write a stack trace starting at the specified block to the specified wcstring
*/ */
void stack_trace(block_t *b, wcstring &buff); void stack_trace(block_t *b, wcstring &buff) const;
int get_block_type(const wchar_t *cmd) const; int get_block_type(const wchar_t *cmd) const;
const wchar_t *get_block_command(int type) const; const wchar_t *get_block_command(int type) const;

View File

@ -2473,7 +2473,8 @@ void reader_run_command(parser_t &parser, const wcstring &cmd)
int reader_shell_test(const wchar_t *b) int reader_shell_test(const wchar_t *b)
{ {
wcstring bstr = b; wcstring bstr = b;
int res = parser_t::principal_parser().detect_errors(bstr); parse_error_list_t errors;
int res = parse_util_detect_errors(bstr, &errors);
if (res & PARSER_TEST_ERROR) if (res & PARSER_TEST_ERROR)
{ {
@ -2490,14 +2491,9 @@ int reader_shell_test(const wchar_t *b)
tmp2, tmp2,
0); 0);
parse_error_list_t errors; wcstring sb;
parser_t::principal_parser().detect_errors(bstr, &errors, L"fish"); parser_t::principal_parser().get_backtrace(bstr, errors, &sb);
fwprintf(stderr, L"%ls", sb.c_str());
if (! errors.empty())
{
const wcstring sb = parse_errors_description(errors, b, L"fish");
fwprintf(stderr, L"%ls", sb.c_str());
}
} }
return res; return res;
} }
@ -3908,13 +3904,14 @@ static int read_ni(int fd, const io_chain_t &io)
} }
parse_error_list_t errors; parse_error_list_t errors;
if (! parser.detect_errors(str, &errors, L"fish")) if (! parse_util_detect_errors(str, &errors))
{ {
parser.eval(str, io, TOP); parser.eval(str, io, TOP);
} }
else else
{ {
const wcstring sb = parse_errors_description(errors, str); wcstring sb;
parser.get_backtrace(str, errors, &sb);
fwprintf(stderr, L"%ls", sb.c_str()); fwprintf(stderr, L"%ls", sb.c_str());
res = 1; res = 1;
} }