From 46a5f19f779bf25cbb74ac0b41ebb7d8a98b05ca Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 24 Jul 2026 09:37:34 -0400 Subject: [PATCH] Port diagnostics format to Prism from CRuby --- ext/prism/extension.c | 370 ++++++++++++++------ include/prism.h | 1 + include/prism/errors_format.h | 53 +++ include/prism/internal/options.h | 12 + include/prism/options.h | 16 + include/prism/serialize.h | 19 ++ lib/prism.rb | 34 +- lib/prism/ffi.rb | 127 ++++++- prism.gemspec | 2 + rakelib/typecheck.rake | 2 + rbi/generated/prism.rbi | 68 ++-- sig/generated/prism.rbs | 34 +- src/errors_format.c | 519 +++++++++++++++++++++++++++++ src/options.c | 14 + src/prism.c | 35 ++ test/prism/api/raise_error_test.rb | 157 +++++++++ test/prism/newline_test.rb | 1 + 17 files changed, 1290 insertions(+), 174 deletions(-) create mode 100644 include/prism/errors_format.h create mode 100644 src/errors_format.c create mode 100644 test/prism/api/raise_error_test.rb diff --git a/ext/prism/extension.c b/ext/prism/extension.c index bd6a7d22c3..e411a49725 100644 --- a/ext/prism/extension.c +++ b/ext/prism/extension.c @@ -39,14 +39,52 @@ ID rb_id_option_frozen_string_literal; ID rb_id_option_line; ID rb_id_option_main_script; ID rb_id_option_partial_script; +ID rb_id_option_raise_error; ID rb_id_option_scopes; ID rb_id_option_version; + ID rb_id_source_for; + ID rb_id_forwarding_positionals; ID rb_id_forwarding_keywords; ID rb_id_forwarding_block; ID rb_id_forwarding_all; +ID rb_id_raise_error_plain; +ID rb_id_raise_error_style; +ID rb_id_raise_error_color; + +/******************************************************************************/ +/* Result struct for working with functions that may need to raise errors. */ +/******************************************************************************/ + +typedef struct { + enum { + RESULT_OK, + RESULT_ERR + } type; + VALUE value; +} result_t; + +static result_t +result_ok(VALUE value) { + return (result_t) { .type = RESULT_OK, .value = value }; +} + +static result_t +result_err(VALUE value) { + return (result_t) { .type = RESULT_ERR, .value = value }; +} + +static VALUE +result_get(result_t result) { + if (result.type == RESULT_OK) { + return result.value; + } else { + rb_exc_raise(result.value); + } +} + /******************************************************************************/ /* IO of Ruby code */ /******************************************************************************/ @@ -66,7 +104,6 @@ check_string(VALUE value) { return RSTRING_PTR(value); } - /******************************************************************************/ /* Building C options from Ruby options */ /******************************************************************************/ @@ -233,6 +270,44 @@ build_options_i(VALUE key, VALUE value, VALUE argument) { if (!NIL_P(value)) pm_options_partial_script_set(options, RTEST(value)); } else if (key_id == rb_id_option_freeze) { if (!NIL_P(value)) pm_options_freeze_set(options, RTEST(value)); + } else if (key_id == rb_id_option_raise_error) { + if (!NIL_P(value)) { + if (value == Qtrue) { + /* When given true, color when $stderr is a terminal (unless + * NO_COLOR is set), bold styling when NO_COLOR is set, plain + * otherwise. + * + * rb_stderr_tty_p is not available to extensions, so instead + * we call the tty? method on $stderr, guarding against it + * having been replaced by an object that does not respond to + * tty?. */ + if (rb_respond_to(rb_stderr, rb_intern_const("tty?")) && RTEST(rb_funcall(rb_stderr, rb_intern_const("tty?"), 0))) { + const char *no_color = getenv("NO_COLOR"); + if (no_color == NULL || no_color[0] == '\0') { + pm_options_raise_error_set(options, (uint8_t) PM_ERRORS_FORMAT_COLOR); + } else { + pm_options_raise_error_set(options, (uint8_t) PM_ERRORS_FORMAT_STYLE); + } + } else { + pm_options_raise_error_set(options, (uint8_t) PM_ERRORS_FORMAT_PLAIN); + } + } else { + if (!SYMBOL_P(value)) { + rb_raise(rb_eTypeError, "wrong argument type %"PRIsVALUE" (expected Symbol)", rb_obj_class(value)); + } + + ID value_id = SYM2ID(value); + if (value_id == rb_id_raise_error_plain) { + pm_options_raise_error_set(options, (uint8_t) PM_ERRORS_FORMAT_PLAIN); + } else if (value_id == rb_id_raise_error_style) { + pm_options_raise_error_set(options, (uint8_t) PM_ERRORS_FORMAT_STYLE); + } else if (value_id == rb_id_raise_error_color) { + pm_options_raise_error_set(options, (uint8_t) PM_ERRORS_FORMAT_COLOR); + } else { + rb_raise(rb_eArgError, "invalid raise_error value: %" PRIsVALUE, value); + } + } + } } else { rb_raise(rb_eArgError, "unknown keyword: %" PRIsVALUE, key); } @@ -358,6 +433,56 @@ file_options(int argc, VALUE *argv, pm_options_t *options, VALUE *encoded_filepa return pm_src; } +/** + * Check if the given parser has any errors and the raise option was set. If + * both are true, format them and return the formatted error wrapped in a + * result. + */ +static result_t +check_raise_error_option(pm_parser_t *parser, const pm_options_t *options, rb_encoding *path_encoding) { + if (pm_parser_errors_size(parser) == 0) return result_ok(Qnil); + + uint8_t raise_error = pm_options_raise_error(options); + if (raise_error == 0) return result_ok(Qnil); + + pm_buffer_t *buffer = pm_buffer_new(); + if (buffer == NULL) { + return result_err(rb_exc_new_cstr(rb_eNoMemError, "failed to allocate memory")); + } + + pm_error_level_t error_level = pm_errors_format(parser, buffer, (pm_errors_format_type_t) raise_error); + + /* Copy the message out of the buffer and free it before building any + * other Ruby objects so that the buffer cannot be leaked if building the + * objects raises. */ + rb_encoding *message_encoding = (error_level == PM_ERROR_LEVEL_LOAD) ? rb_locale_encoding() : rb_enc_find(pm_parser_encoding_name(parser)); + VALUE message = rb_enc_str_new(pm_buffer_value(buffer), (long) pm_buffer_length(buffer), message_encoding); + pm_buffer_free(buffer); + + VALUE error = Qnil; + switch (error_level) { + case PM_ERROR_LEVEL_SYNTAX: { + error = rb_exc_new_str(rb_eSyntaxError, message); + + rb_encoding *path_encoding_normal = path_encoding == NULL ? rb_utf8_encoding() : path_encoding; + VALUE path = rb_enc_str_new((const char *) pm_string_source(pm_options_filepath(options)), pm_string_length(pm_options_filepath(options)), path_encoding_normal); + rb_ivar_set(error, rb_intern_const("@path"), path); + break; + } + case PM_ERROR_LEVEL_ARGUMENT: { + error = rb_exc_new_str(rb_eArgError, message); + break; + } + case PM_ERROR_LEVEL_LOAD: { + error = rb_exc_new_str(rb_eLoadError, message); + rb_ivar_set(error, rb_intern_const("@path"), Qnil); + break; + } + } + + return result_err(error); +} + #ifndef PRISM_EXCLUDE_SERIALIZATION /******************************************************************************/ @@ -367,21 +492,26 @@ file_options(int argc, VALUE *argv, pm_options_t *options, VALUE *encoded_filepa /** * Dump the AST corresponding to the given input to a string. */ -static VALUE -dump_input(const uint8_t *input, size_t input_length, const pm_options_t *options) { - pm_buffer_t *buffer = pm_buffer_new(); - if (!buffer) { - rb_raise(rb_eNoMemError, "failed to allocate memory"); - } - +static result_t +dump_input(const uint8_t *input, size_t input_length, const pm_options_t *options, rb_encoding *path_encoding) { pm_arena_t *arena = pm_arena_new(); pm_parser_t *parser = pm_parser_new(arena, input, input_length, options); - pm_node_t *node = pm_parse(parser); - pm_serialize(parser, node, buffer); - VALUE result = rb_str_new(pm_buffer_value(buffer), pm_buffer_length(buffer)); - pm_buffer_free(buffer); + result_t result = check_raise_error_option(parser, options, path_encoding); + if (result.type == RESULT_OK) { + pm_buffer_t *buffer = pm_buffer_new(); + if (buffer) { + pm_serialize(parser, node, buffer); + result = result_ok(rb_str_new(pm_buffer_value(buffer), pm_buffer_length(buffer))); + pm_buffer_free(buffer); + + if (pm_options_freeze(options)) rb_obj_freeze(result.value); + } else { + result = result_err(rb_exc_new_cstr(rb_eNoMemError, "failed to allocate memory")); + } + } + pm_parser_free(parser); pm_arena_free(arena); @@ -410,8 +540,7 @@ dump(int argc, VALUE *argv, VALUE self) { source = (const uint8_t *) dup; #endif - VALUE value = dump_input(source, length, options); - if (pm_options_freeze(options)) rb_obj_freeze(value); + result_t result = dump_input(source, length, options, NULL); #ifdef PRISM_BUILD_DEBUG #ifdef xfree_sized @@ -422,8 +551,7 @@ dump(int argc, VALUE *argv, VALUE self) { #endif pm_options_free(options); - - return value; + return result_get(result); } /** @@ -441,11 +569,11 @@ dump_file(int argc, VALUE *argv, VALUE self) { VALUE encoded_filepath; pm_source_t *src = file_options(argc, argv, options, &encoded_filepath); - VALUE value = dump_input(pm_source_source(src), pm_source_length(src), options); + result_t result = dump_input(pm_source_source(src), pm_source_length(src), options, rb_enc_get(encoded_filepath)); pm_source_free(src); pm_options_free(options); - return value; + return result_get(result); } #endif @@ -780,8 +908,8 @@ parse_lex_encoding_changed_callback(pm_parser_t *parser) { * Parse the given input and return a ParseResult containing just the tokens or * the nodes and tokens. */ -static VALUE -parse_lex_input(const uint8_t *input, size_t input_length, const pm_options_t *options, bool return_nodes) { +static result_t +parse_lex_input(const uint8_t *input, size_t input_length, const pm_options_t *options, rb_encoding *path_encoding, bool return_nodes) { pm_arena_t *arena = pm_arena_new(); pm_parser_t *parser = pm_parser_new(arena, input, input_length, options); pm_parser_encoding_changed_callback_set(parser, parse_lex_encoding_changed_callback); @@ -802,32 +930,34 @@ parse_lex_input(const uint8_t *input, size_t input_length, const pm_options_t *o pm_node_t *node = pm_parse(parser); - /* Update the Source object with the correct encoding and line offsets, - * which are only available after pm_parse() completes. */ - rb_encoding *encoding = rb_enc_find(pm_parser_encoding_name(parser)); - rb_enc_associate(source_string, encoding); + result_t result = check_raise_error_option(parser, options, path_encoding); + if (result.type == RESULT_OK) { + /* Update the Source object with the correct encoding and line offsets, + * which are only available after pm_parse() completes. */ + rb_encoding *encoding = rb_enc_find(pm_parser_encoding_name(parser)); + rb_enc_associate(source_string, encoding); - const pm_line_offset_list_t *line_offsets = pm_parser_line_offsets(parser); - for (size_t index = 0; index < line_offsets->size; index++) { - rb_ary_store(offsets, (long) index, ULONG2NUM(line_offsets->offsets[index])); - } + const pm_line_offset_list_t *line_offsets = pm_parser_line_offsets(parser); + for (size_t index = 0; index < line_offsets->size; index++) { + rb_ary_store(offsets, (long) index, ULONG2NUM(line_offsets->offsets[index])); + } - if (pm_options_freeze(options)) { - rb_obj_freeze(source_string); - rb_obj_freeze(offsets); - rb_obj_freeze(source); - rb_obj_freeze(parse_lex_data.tokens); - } + if (pm_options_freeze(options)) { + rb_obj_freeze(source_string); + rb_obj_freeze(offsets); + rb_obj_freeze(source); + rb_obj_freeze(parse_lex_data.tokens); + } - VALUE result; - if (return_nodes) { - VALUE value = rb_ary_new_capa(2); - rb_ary_push(value, pm_ast_new(parser, node, parse_lex_data.encoding, source, pm_options_freeze(options))); - rb_ary_push(value, parse_lex_data.tokens); - if (pm_options_freeze(options)) rb_obj_freeze(value); - result = parse_result_create(rb_cPrismParseLexResult, parser, value, parse_lex_data.encoding, source, pm_options_freeze(options)); - } else { - result = parse_result_create(rb_cPrismLexResult, parser, parse_lex_data.tokens, parse_lex_data.encoding, source, pm_options_freeze(options)); + if (return_nodes) { + VALUE value = rb_ary_new_capa(2); + rb_ary_push(value, pm_ast_new(parser, node, parse_lex_data.encoding, source, pm_options_freeze(options))); + rb_ary_push(value, parse_lex_data.tokens); + if (pm_options_freeze(options)) rb_obj_freeze(value); + result = result_ok(parse_result_create(rb_cPrismParseLexResult, parser, value, parse_lex_data.encoding, source, pm_options_freeze(options))); + } else { + result = result_ok(parse_result_create(rb_cPrismLexResult, parser, parse_lex_data.tokens, parse_lex_data.encoding, source, pm_options_freeze(options))); + } } pm_parser_free(parser); @@ -849,10 +979,10 @@ lex(int argc, VALUE *argv, VALUE self) { pm_options_t *options = pm_options_new(); VALUE string = string_options(argc, argv, options); - VALUE result = parse_lex_input((const uint8_t *) RSTRING_PTR(string), RSTRING_LEN(string), options, false); + result_t result = parse_lex_input((const uint8_t *) RSTRING_PTR(string), RSTRING_LEN(string), options, NULL, false); pm_options_free(options); - return result; + return result_get(result); } /** @@ -870,11 +1000,11 @@ lex_file(int argc, VALUE *argv, VALUE self) { VALUE encoded_filepath; pm_source_t *src = file_options(argc, argv, options, &encoded_filepath); - VALUE value = parse_lex_input(pm_source_source(src), pm_source_length(src), options, false); + result_t result = parse_lex_input(pm_source_source(src), pm_source_length(src), options, rb_enc_get(encoded_filepath), false); pm_source_free(src); pm_options_free(options); - return value; + return result_get(result); } /******************************************************************************/ @@ -884,21 +1014,25 @@ lex_file(int argc, VALUE *argv, VALUE self) { /** * Parse the given input and return a ParseResult instance. */ -static VALUE -parse_input(const uint8_t *input, size_t input_length, const pm_options_t *options) { +static result_t +parse_input(const uint8_t *input, size_t input_length, const pm_options_t *options, rb_encoding *path_encoding) { pm_arena_t *arena = pm_arena_new(); pm_parser_t *parser = pm_parser_new(arena, input, input_length, options); pm_node_t *node = pm_parse(parser); - rb_encoding *encoding = rb_enc_find(pm_parser_encoding_name(parser)); - bool freeze = pm_options_freeze(options); - VALUE source = pm_source_new(parser, encoding, freeze); - VALUE value = pm_ast_new(parser, node, encoding, source, freeze); - VALUE result = parse_result_create(rb_cPrismParseResult, parser, value, encoding, source, freeze); + result_t result = check_raise_error_option(parser, options, path_encoding); + if (result.type == RESULT_OK) { + rb_encoding *encoding = rb_enc_find(pm_parser_encoding_name(parser)); - if (freeze) { - rb_obj_freeze(source); + bool freeze = pm_options_freeze(options); + VALUE source = pm_source_new(parser, encoding, freeze); + VALUE value = pm_ast_new(parser, node, encoding, source, freeze); + result = result_ok(parse_result_create(rb_cPrismParseResult, parser, value, encoding, source, freeze)); + + if (freeze) { + rb_obj_freeze(source); + } } pm_parser_free(parser); @@ -939,6 +1073,20 @@ parse_input(const uint8_t *input, size_t input_length, const pm_options_t *optio * script that you know will be embedded inside another script later, but * you do not have that context yet. For example, when parsing an ERB * template that will be evaluated inside another script. + * * `raise_error` - either nil, true, or a symbol indicating that an error + * should be raised if the source contains any errors. The message of + * the error will include the source of the lines that contain errors + * when possible, and the value of this option controls how those lines + * are formatted. Valid values are `:plain` (no formatting), `:style` + * (bold formatting), and `:color` (bold and color formatting). When + * `true` is given, the formatting is determined by whether or not + * `$stderr` is a terminal and whether or not the `NO_COLOR` environment + * variable is set, mirroring the behavior of CRuby itself. Syntax-level + * errors raise SyntaxError, argument-level errors raise ArgumentError, + * and load-level errors raise LoadError. Note that this option is + * honored by every API that accepts these options, including predicates + * like Prism.parse_success?, which will raise instead of returning a + * boolean when the source contains errors. * * `scopes` - the locals that are in scope surrounding the code that is being * parsed. This should be an array of arrays of symbols or nil. Scopes are * ordered from the outermost scope to the innermost one. @@ -966,7 +1114,7 @@ parse(int argc, VALUE *argv, VALUE self) { source = (const uint8_t *) dup; #endif - VALUE value = parse_input(source, length, options); + result_t result = parse_input(source, length, options, NULL); #ifdef PRISM_BUILD_DEBUG #ifdef xfree_sized @@ -977,7 +1125,7 @@ parse(int argc, VALUE *argv, VALUE self) { #endif pm_options_free(options); - return value; + return result_get(result); } /** @@ -995,24 +1143,28 @@ parse_file(int argc, VALUE *argv, VALUE self) { VALUE encoded_filepath; pm_source_t *src = file_options(argc, argv, options, &encoded_filepath); - VALUE value = parse_input(pm_source_source(src), pm_source_length(src), options); + result_t result = parse_input(pm_source_source(src), pm_source_length(src), options, rb_enc_get(encoded_filepath)); pm_source_free(src); pm_options_free(options); - return value; + return result_get(result); } /** * Parse the given input and return nothing. */ -static void -profile_input(const uint8_t *input, size_t input_length, const pm_options_t *options) { +static result_t +profile_input(const uint8_t *input, size_t input_length, const pm_options_t *options, rb_encoding *path_encoding) { pm_arena_t *arena = pm_arena_new(); pm_parser_t *parser = pm_parser_new(arena, input, input_length, options); pm_parse(parser); + + result_t result = check_raise_error_option(parser, options, path_encoding); pm_parser_free(parser); pm_arena_free(arena); + + return result; } /** @@ -1029,8 +1181,9 @@ profile(int argc, VALUE *argv, VALUE self) { pm_options_t *options = pm_options_new(); VALUE string = string_options(argc, argv, options); - profile_input((const uint8_t *) RSTRING_PTR(string), RSTRING_LEN(string), options); + result_t result = profile_input((const uint8_t *) RSTRING_PTR(string), RSTRING_LEN(string), options, NULL); pm_options_free(options); + result_get(result); return Qnil; } @@ -1051,9 +1204,10 @@ profile_file(int argc, VALUE *argv, VALUE self) { VALUE encoded_filepath; pm_source_t *src = file_options(argc, argv, options, &encoded_filepath); - profile_input(pm_source_source(src), pm_source_length(src), options); + result_t result = profile_input(pm_source_source(src), pm_source_length(src), options, rb_enc_get(encoded_filepath)); pm_source_free(src); pm_options_free(options); + result_get(result); return Qnil; } @@ -1147,38 +1301,46 @@ parse_stream(int argc, VALUE *argv, VALUE self) { pm_parser_t *parser; pm_node_t *node = pm_parse_stream(&parser, arena, src, options); - rb_encoding *encoding = rb_enc_find(pm_parser_encoding_name(parser)); - VALUE source = pm_source_new(parser, encoding, pm_options_freeze(options)); - VALUE value = pm_ast_new(parser, node, encoding, source, pm_options_freeze(options)); - VALUE result = parse_result_create(rb_cPrismParseResult, parser, value, encoding, source, pm_options_freeze(options)); + result_t result = check_raise_error_option(parser, options, NULL); + if (result.type == RESULT_OK) { + rb_encoding *encoding = rb_enc_find(pm_parser_encoding_name(parser)); + + VALUE source = pm_source_new(parser, encoding, pm_options_freeze(options)); + VALUE value = pm_ast_new(parser, node, encoding, source, pm_options_freeze(options)); + result = result_ok(parse_result_create(rb_cPrismParseResult, parser, value, encoding, source, pm_options_freeze(options))); + } pm_source_free(src); pm_parser_free(parser); pm_arena_free(arena); pm_options_free(options); - return result; + return result_get(result); } /** * Parse the given input and return an array of Comment objects. */ -static VALUE -parse_input_comments(const uint8_t *input, size_t input_length, const pm_options_t *options) { +static result_t +parse_input_comments(const uint8_t *input, size_t input_length, const pm_options_t *options, rb_encoding *path_encoding) { pm_arena_t *arena = pm_arena_new(); pm_parser_t *parser = pm_parser_new(arena, input, input_length, options); pm_parse(parser); - rb_encoding *encoding = rb_enc_find(pm_parser_encoding_name(parser)); - VALUE source = pm_source_new(parser, encoding, pm_options_freeze(options)); - VALUE comments = parser_comments(parser, source, pm_options_freeze(options)); + result_t result = check_raise_error_option(parser, options, path_encoding); + if (result.type == RESULT_OK) { + rb_encoding *encoding = rb_enc_find(pm_parser_encoding_name(parser)); + + VALUE source = pm_source_new(parser, encoding, pm_options_freeze(options)); + result = result_ok(parser_comments(parser, source, pm_options_freeze(options))); + } pm_parser_free(parser); pm_arena_free(arena); - return comments; + return result; } /** @@ -1194,10 +1356,10 @@ parse_comments(int argc, VALUE *argv, VALUE self) { pm_options_t *options = pm_options_new(); VALUE string = string_options(argc, argv, options); - VALUE result = parse_input_comments((const uint8_t *) RSTRING_PTR(string), RSTRING_LEN(string), options); + result_t result = parse_input_comments((const uint8_t *) RSTRING_PTR(string), RSTRING_LEN(string), options, NULL); pm_options_free(options); - return result; + return result_get(result); } /** @@ -1215,11 +1377,11 @@ parse_file_comments(int argc, VALUE *argv, VALUE self) { VALUE encoded_filepath; pm_source_t *src = file_options(argc, argv, options, &encoded_filepath); - VALUE value = parse_input_comments(pm_source_source(src), pm_source_length(src), options); + result_t result = parse_input_comments(pm_source_source(src), pm_source_length(src), options, rb_enc_get(encoded_filepath)); pm_source_free(src); pm_options_free(options); - return value; + return result_get(result); } /** @@ -1242,10 +1404,10 @@ parse_lex(int argc, VALUE *argv, VALUE self) { pm_options_t *options = pm_options_new(); VALUE string = string_options(argc, argv, options); - VALUE value = parse_lex_input((const uint8_t *) RSTRING_PTR(string), RSTRING_LEN(string), options, true); + result_t result = parse_lex_input((const uint8_t *) RSTRING_PTR(string), RSTRING_LEN(string), options, NULL, true); pm_options_free(options); - return value; + return result_get(result); } /** @@ -1270,24 +1432,27 @@ parse_lex_file(int argc, VALUE *argv, VALUE self) { VALUE encoded_filepath; pm_source_t *src = file_options(argc, argv, options, &encoded_filepath); - VALUE value = parse_lex_input(pm_source_source(src), pm_source_length(src), options, true); + result_t result = parse_lex_input(pm_source_source(src), pm_source_length(src), options, rb_enc_get(encoded_filepath), true); pm_source_free(src); pm_options_free(options); - return value; + return result_get(result); } /** * Parse the given input and return true if it parses without errors. */ -static VALUE -parse_input_success_p(const uint8_t *input, size_t input_length, const pm_options_t *options) { +static result_t +parse_input_success_p(const uint8_t *input, size_t input_length, const pm_options_t *options, rb_encoding *path_encoding) { pm_arena_t *arena = pm_arena_new(); pm_parser_t *parser = pm_parser_new(arena, input, input_length, options); - pm_parse(parser); - VALUE result = pm_parser_errors_size(parser) == 0 ? Qtrue : Qfalse; + result_t result = check_raise_error_option(parser, options, path_encoding); + if (result.type == RESULT_OK) { + result = result_ok(pm_parser_errors_size(parser) == 0 ? Qtrue : Qfalse); + } + pm_parser_free(parser); pm_arena_free(arena); @@ -1307,10 +1472,10 @@ parse_success_p(int argc, VALUE *argv, VALUE self) { pm_options_t *options = pm_options_new(); VALUE string = string_options(argc, argv, options); - VALUE result = parse_input_success_p((const uint8_t *) RSTRING_PTR(string), RSTRING_LEN(string), options); + result_t result = parse_input_success_p((const uint8_t *) RSTRING_PTR(string), RSTRING_LEN(string), options, NULL); pm_options_free(options); - return result; + return result_get(result); } /** @@ -1341,11 +1506,11 @@ parse_file_success_p(int argc, VALUE *argv, VALUE self) { VALUE encoded_filepath; pm_source_t *src = file_options(argc, argv, options, &encoded_filepath); - VALUE result = parse_input_success_p(pm_source_source(src), pm_source_length(src), options); + result_t result = parse_input_success_p(pm_source_source(src), pm_source_length(src), options, rb_enc_get(encoded_filepath)); pm_source_free(src); pm_options_free(options); - return result; + return result_get(result); } /** @@ -1435,8 +1600,8 @@ string_query_method_name_p(VALUE self, VALUE string) { */ RUBY_FUNC_EXPORTED void Init_prism(void) { - // Make sure that the prism library version matches the expected version. - // Otherwise something was compiled incorrectly. + /* Make sure that the prism library version matches the expected version. + * Otherwise something was compiled incorrectly. */ if (strcmp(pm_version(), EXPECTED_PRISM_VERSION) != 0) { rb_raise( rb_eRuntimeError, @@ -1447,12 +1612,12 @@ Init_prism(void) { } #ifdef HAVE_RB_EXT_RACTOR_SAFE - // Mark this extension as Ractor-safe. + /* Mark this extension as Ractor-safe. */ rb_ext_ractor_safe(true); #endif - // Grab up references to all of the constants that we're going to need to - // reference throughout this extension. + /* Grab up references to all of the constants that we are going to need to + * reference throughout this extension. */ rb_cPrism = rb_define_module("Prism"); rb_cPrismNode = rb_define_class_under(rb_cPrism, "Node", rb_cObject); rb_cPrismSource = rb_define_class_under(rb_cPrism, "Source", rb_cObject); @@ -1473,8 +1638,8 @@ Init_prism(void) { rb_cPrismCurrentVersionError = rb_const_get(rb_cPrism, rb_intern("CurrentVersionError")); - // Intern all of the IDs eagerly that we support so that we don't have to do - // it every time we parse. + /* Intern all of the IDs eagerly that we support so that we do not have to + * do it every time we parse. */ rb_id_option_command_line = rb_intern_const("command_line"); rb_id_option_encoding = rb_intern_const("encoding"); rb_id_option_filepath = rb_intern_const("filepath"); @@ -1483,20 +1648,26 @@ Init_prism(void) { rb_id_option_line = rb_intern_const("line"); rb_id_option_main_script = rb_intern_const("main_script"); rb_id_option_partial_script = rb_intern_const("partial_script"); + rb_id_option_raise_error = rb_intern_const("raise_error"); rb_id_option_scopes = rb_intern_const("scopes"); rb_id_option_version = rb_intern_const("version"); + rb_id_source_for = rb_intern("for"); + rb_id_forwarding_positionals = rb_intern("*"); rb_id_forwarding_keywords = rb_intern("**"); rb_id_forwarding_block = rb_intern("&"); rb_id_forwarding_all = rb_intern("..."); + rb_id_raise_error_plain = rb_intern_const("plain"); + rb_id_raise_error_style = rb_intern_const("style"); + rb_id_raise_error_color = rb_intern_const("color"); + /** * The version of the prism library. */ rb_define_const(rb_cPrism, "VERSION", rb_str_freeze(rb_str_new_cstr(EXPECTED_PRISM_VERSION))); - // First, the functions that have to do with lexing and parsing. rb_define_singleton_method(rb_cPrism, "lex", lex, -1); rb_define_singleton_method(rb_cPrism, "lex_file", lex_file, -1); rb_define_singleton_method(rb_cPrism, "parse", parse, -1); @@ -1522,6 +1693,5 @@ Init_prism(void) { rb_define_singleton_method(rb_cPrismStringQuery, "constant?", string_query_constant_p, 1); rb_define_singleton_method(rb_cPrismStringQuery, "method_name?", string_query_method_name_p, 1); - // Next, initialize the other APIs. Init_prism_api_node(); } diff --git a/include/prism.h b/include/prism.h index b342bb32c6..14ef7e3076 100644 --- a/include/prism.h +++ b/include/prism.h @@ -14,6 +14,7 @@ extern "C" { #include "prism/ast.h" #include "prism/buffer.h" #include "prism/diagnostic.h" +#include "prism/errors_format.h" #include "prism/json.h" #include "prism/node.h" #include "prism/options.h" diff --git a/include/prism/errors_format.h b/include/prism/errors_format.h new file mode 100644 index 0000000000..cca7cdd941 --- /dev/null +++ b/include/prism/errors_format.h @@ -0,0 +1,53 @@ +/** + * @file errors_format.h + * + * A function for formatting the errors in a parser into a buffer. + */ +#ifndef PRISM_ERRORS_FORMAT_H +#define PRISM_ERRORS_FORMAT_H + +#include "prism/compiler/exported.h" +#include "prism/compiler/nodiscard.h" +#include "prism/compiler/nonnull.h" + +#include "prism/buffer.h" +#include "prism/diagnostic.h" +#include "prism/parser.h" + +/** The type of formatting to use when formatting errors. */ +typedef enum { + /** Format errors in a plain format with no colors or styles. Typically + * used when outputting to a file or when the output is not a terminal. + */ + PM_ERRORS_FORMAT_PLAIN = 1, + + /** Format errors in a style format with bold only. Typically used when + * outputting to a terminal where you do not want colors. + */ + PM_ERRORS_FORMAT_STYLE = 2, + + /** Format errors in a color format with bold and colors. The default case + * when outputting on the terminal. + */ + PM_ERRORS_FORMAT_COLOR = 3 +} pm_errors_format_type_t; + +/** + * Format the errors in a parser into a buffer. After return, the buffer will + * contain the formatted errors and any relevant source snippets if available + * and possible. + * + * @param parser The parser containing the errors to format. Note that the + * parser does not need to be fully initialized, but it will need to have + * start, end, start_line, encoding, line_offsets, filepath, and error_list + * initialized. + * @param buffer The buffer to format the errors into. + * @param format_type The type of formatting to use when formatting the errors. + * @returns The error level of the error with the highest precedence that was + * formatted. In practice this means it will be the argument or load error + * level if any were found, otherwise it will be the syntax error level. + * Callers should raise appropriate error types based on the returned level. + */ +PRISM_EXPORTED_FUNCTION pm_error_level_t pm_errors_format(const pm_parser_t *parser, pm_buffer_t *buffer, pm_errors_format_type_t format_type) PRISM_NONNULL(1, 2); + +#endif diff --git a/include/prism/internal/options.h b/include/prism/internal/options.h index 7e37742a8b..4ac28808f4 100644 --- a/include/prism/internal/options.h +++ b/include/prism/internal/options.h @@ -105,6 +105,18 @@ struct pm_options_t { */ int8_t frozen_string_literal; + /* + * Whether or not the parser should raise an error when it encounters an + * error. By default this is off, but can be turned on in the case that you + * want nicely formatted syntax errors. The values that this can be set to + * are: + * - 0 - do not raise an error, just return the errors in the result + * - PM_ERRORS_FORMAT_PLAIN - raise a plain error with no formatting + * - PM_ERRORS_FORMAT_STYLE - raise an error with bold formatting + * - PM_ERRORS_FORMAT_COLOR - raise an error with color formatting + */ + uint8_t raise_error; + /* * Whether or not the encoding magic comments should be respected. This is a * niche use-case where you want to parse a file with a specific encoding diff --git a/include/prism/options.h b/include/prism/options.h index 0f5d7529b1..71e606bb5d 100644 --- a/include/prism/options.h +++ b/include/prism/options.h @@ -249,6 +249,22 @@ PRISM_EXPORTED_FUNCTION bool pm_options_freeze(const pm_options_t *options) PRIS */ PRISM_EXPORTED_FUNCTION void pm_options_freeze_set(pm_options_t *options, bool freeze) PRISM_NONNULL(1); +/** + * Get the raise_error option on the given options struct. + * + * @param options The options struct to get the raise_error value from. + * @returns The raise_error value. + */ +PRISM_EXPORTED_FUNCTION uint8_t pm_options_raise_error(const pm_options_t *options) PRISM_NONNULL(1); + +/** + * Set the raise_error option on the given options struct. + * + * @param options The options struct to set the raise_error value on. + * @param raise_error The raise_error value to set. + */ +PRISM_EXPORTED_FUNCTION void pm_options_raise_error_set(pm_options_t *options, uint8_t raise_error) PRISM_NONNULL(1); + /** * Allocate and zero out the scopes array on the given options struct. * diff --git a/include/prism/serialize.h b/include/prism/serialize.h index 786a1514bc..f401e7ba97 100644 --- a/include/prism/serialize.h +++ b/include/prism/serialize.h @@ -17,6 +17,7 @@ #include "prism/compiler/nonnull.h" #include "prism/buffer.h" +#include "prism/errors_format.h" #include "prism/parser.h" #include "prism/source.h" #include "prism/stream.h" @@ -91,6 +92,24 @@ PRISM_EXPORTED_FUNCTION void pm_serialize_parse_lex(pm_buffer_t *buffer, const u */ PRISM_EXPORTED_FUNCTION bool pm_serialize_parse_success_p(const uint8_t *source, size_t size, const char *data) PRISM_NONNULL(1); +/** + * Parse the given source and format any errors that are encountered into the + * given buffer using the given format type. If the source parses without any + * errors, then -1 is returned and the buffer is left empty. Otherwise, the + * name of the encoding of the source is written to the buffer, followed by a + * null byte, followed by the formatted errors, and the error level of the + * error with the highest precedence is returned. + * + * @param buffer The buffer to write the encoding name and formatted errors to. + * @param source The source to parse. + * @param size The size of the source. + * @param data The optional data to pass to the parser. + * @param format_type The type of formatting to use when formatting the errors. + * @returns The error level of the error with the highest precedence, or -1 if + * the source parsed without errors. + */ +PRISM_EXPORTED_FUNCTION int8_t pm_serialize_parse_errors_format(pm_buffer_t *buffer, const uint8_t *source, size_t size, const char *data, pm_errors_format_type_t format_type) PRISM_NONNULL(1, 2); + #endif #endif diff --git a/lib/prism.rb b/lib/prism.rb index 9189c449d7..8dc8050cc4 100644 --- a/lib/prism.rb +++ b/lib/prism.rb @@ -103,23 +103,23 @@ def self.find(callable, rubyvm: !!defined?(RubyVM)) # def gets: (?Integer integer) -> (String | nil) # end # - # def self.parse: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult - # def self.profile: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> void - # def self.lex: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> LexResult - # def self.parse_lex: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseLexResult - # def self.dump: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> String - # def self.parse_comments: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> Array[Comment] - # def self.parse_success?: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool - # def self.parse_failure?: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool - # def self.parse_stream: (_Stream stream, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult - # def self.parse_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult - # def self.profile_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> void - # def self.lex_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> LexResult - # def self.parse_lex_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseLexResult - # def self.dump_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> String - # def self.parse_file_comments: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> Array[Comment] - # def self.parse_file_success?: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool - # def self.parse_file_failure?: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool + # def self.parse: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult + # def self.profile: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> void + # def self.lex: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> LexResult + # def self.parse_lex: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseLexResult + # def self.dump: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> String + # def self.parse_comments: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> Array[Comment] + # def self.parse_success?: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> bool + # def self.parse_failure?: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> bool + # def self.parse_stream: (_Stream stream, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult + # def self.parse_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult + # def self.profile_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> void + # def self.lex_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> LexResult + # def self.parse_lex_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseLexResult + # def self.dump_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> String + # def self.parse_file_comments: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> Array[Comment] + # def self.parse_file_success?: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> bool + # def self.parse_file_failure?: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> bool end require_relative "prism/polyfill/byteindex" diff --git a/lib/prism/ffi.rb b/lib/prism/ffi.rb index 01c1f4ace4..d717925cc0 100644 --- a/lib/prism/ffi.rb +++ b/lib/prism/ffi.rb @@ -93,6 +93,7 @@ def self.load_exported_functions_from(header, *functions, callbacks) pm_source_init_result_values = %i[PM_SOURCE_INIT_SUCCESS PM_SOURCE_INIT_ERROR_GENERIC PM_SOURCE_INIT_ERROR_DIRECTORY PM_SOURCE_INIT_ERROR_NON_REGULAR] enum :pm_source_init_result_t, pm_source_init_result_values enum :pm_string_query_t, [:PM_STRING_QUERY_ERROR, -1, :PM_STRING_QUERY_FALSE, :PM_STRING_QUERY_TRUE] + enum :pm_errors_format_type_t, [:PM_ERRORS_FORMAT_PLAIN, 1, :PM_ERRORS_FORMAT_STYLE, :PM_ERRORS_FORMAT_COLOR] # Ractor-safe lookup table for pm_source_init_result_t, since FFI's # enum_type accesses module instance variables that are not shareable. @@ -112,6 +113,7 @@ def self.load_exported_functions_from(header, *functions, callbacks) "pm_serialize_lex", "pm_serialize_parse_lex", "pm_serialize_parse_success_p", + "pm_serialize_parse_errors_format", [] ) @@ -293,6 +295,8 @@ def parse_file(filepath, **options) # Mirror the Prism.parse_stream API by using the serialization API. def parse_stream(stream, **options) + format_type = raise_error_format_type(options) + LibRubyParser::PrismBuffer.with do |buffer| # The largest number of bytes a single character can occupy in any # encoding Ruby supports. IO#gets(limit) may return up to @@ -321,7 +325,15 @@ def parse_stream(stream, **options) pm_source = LibRubyParser.pm_source_stream_new(nil, callback, eof_callback) begin LibRubyParser.pm_serialize_parse_stream(buffer.pointer, pm_source, dump_options(options)) - Prism.load(source, buffer.read, options.fetch(:freeze, false)) + result = Prism.load(source, buffer.read, options.fetch(:freeze, false)) + + if format_type && result.failure? + LibRubyParser::PrismSource.with_string(source) do |string| + raise_error(string, options, format_type) + end + end + + result ensure LibRubyParser.pm_source_free(pm_source) if pm_source && !pm_source.null? end @@ -376,6 +388,10 @@ def parse_file_failure?(filepath, **options) # Mirror the Prism.profile API by using the serialization API. def profile(source, **options) LibRubyParser::PrismSource.with_string(source) do |string| + if (format_type = raise_error_format_type(options)) + raise_error(string, options, format_type) + end + LibRubyParser::PrismBuffer.with do |buffer| LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options)) nil @@ -386,8 +402,13 @@ def profile(source, **options) # Mirror the Prism.profile_file API by using the serialization API. def profile_file(filepath, **options) LibRubyParser::PrismSource.with_file(filepath) do |string| + options[:filepath] = filepath + + if (format_type = raise_error_format_type(options)) + raise_error(string, options, format_type) + end + LibRubyParser::PrismBuffer.with do |buffer| - options[:filepath] = filepath LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options)) nil end @@ -397,6 +418,10 @@ def profile_file(filepath, **options) private def dump_common(string, options) # :nodoc: + if (format_type = raise_error_format_type(options)) + raise_error(string, options, format_type) + end + LibRubyParser::PrismBuffer.with do |buffer| LibRubyParser.pm_serialize_parse(buffer.pointer, string.pointer, string.length, dump_options(options)) @@ -408,18 +433,31 @@ def dump_common(string, options) # :nodoc: end def lex_common(string, code, options) # :nodoc: + format_type = raise_error_format_type(options) + LibRubyParser::PrismBuffer.with do |buffer| LibRubyParser.pm_serialize_lex(buffer.pointer, string.pointer, string.length, dump_options(options)) - Serialize.load_lex(code, buffer.read, options.fetch(:freeze, false)) + result = Serialize.load_lex(code, buffer.read, options.fetch(:freeze, false)) + + raise_error(string, options, format_type) if format_type && result.failure? + result end end def parse_common(string, code, options) # :nodoc: + format_type = raise_error_format_type(options) serialized = dump_common(string, options) - Serialize.load_parse(code, serialized, options.fetch(:freeze, false)) + result = Serialize.load_parse(code, serialized, options.fetch(:freeze, false)) + + raise_error(string, options, format_type) if format_type && result.failure? + result end def parse_comments_common(string, code, options) # :nodoc: + if (format_type = raise_error_format_type(options)) + raise_error(string, options, format_type) + end + LibRubyParser::PrismBuffer.with do |buffer| LibRubyParser.pm_serialize_parse_comments(buffer.pointer, string.pointer, string.length, dump_options(options)) Serialize.load_parse_comments(code, buffer.read, options.fetch(:freeze, false)) @@ -427,14 +465,82 @@ def parse_comments_common(string, code, options) # :nodoc: end def parse_lex_common(string, code, options) # :nodoc: + format_type = raise_error_format_type(options) + LibRubyParser::PrismBuffer.with do |buffer| LibRubyParser.pm_serialize_parse_lex(buffer.pointer, string.pointer, string.length, dump_options(options)) - Serialize.load_parse_lex(code, buffer.read, options.fetch(:freeze, false)) + result = Serialize.load_parse_lex(code, buffer.read, options.fetch(:freeze, false)) + + raise_error(string, options, format_type) if format_type && result.failure? + result end end def parse_file_success_common(string, options) # :nodoc: - LibRubyParser.pm_serialize_parse_success_p(string.pointer, string.length, dump_options(options)) + format_type = raise_error_format_type(options) + success = LibRubyParser.pm_serialize_parse_success_p(string.pointer, string.length, dump_options(options)) + + raise_error(string, options, format_type) if format_type && !success + success + end + + # Extract the raise_error option from the given options hash and convert + # it into the format type that should be used when formatting errors, or + # nil if raising is disabled. + def raise_error_format_type(options) # :nodoc: + case (value = options.delete(:raise_error)) + when nil, false + nil + when true + # When given true, mirror the behavior of CRuby itself: color when + # $stderr is a terminal (unless NO_COLOR is set), bold styling when + # NO_COLOR is set, plain otherwise. This policy is duplicated in + # ext/prism/extension.c (build_options_i) and the two must stay in + # sync. + if $stderr.respond_to?(:tty?) && $stderr.tty? + no_color = ENV["NO_COLOR"] + (no_color.nil? || no_color.empty?) ? :PM_ERRORS_FORMAT_COLOR : :PM_ERRORS_FORMAT_STYLE + else + :PM_ERRORS_FORMAT_PLAIN + end + when :plain + :PM_ERRORS_FORMAT_PLAIN + when :style + :PM_ERRORS_FORMAT_STYLE + when :color + :PM_ERRORS_FORMAT_COLOR + when Symbol + raise ArgumentError, "invalid raise_error value: #{value}" + else + raise TypeError, "wrong argument type #{value.class} (expected Symbol)" + end + end + + # Parse the given source and format any errors that are encountered into + # an appropriate exception, then raise it. If the source parses without + # any errors, then return nil. + def raise_error(string, options, format_type) # :nodoc: + LibRubyParser::PrismBuffer.with do |buffer| + level = LibRubyParser.pm_serialize_parse_errors_format(buffer.pointer, string.pointer, string.length, dump_options(options), format_type) + return if level == -1 + + encoding_name, _, message = buffer.read.partition("\0") + + case level + when 0 # syntax + error = SyntaxError.new(message.force_encoding(encoding_name)) + error.instance_variable_set(:@path, options.fetch(:filepath, "")) + raise error + when 1 # argument + raise ArgumentError, message.force_encoding(encoding_name) + when 2 # load + error = LoadError.new(message.force_encoding(Encoding.find("locale"))) + error.instance_variable_set(:@path, nil) + raise error + else + raise "Unknown error level: #{level}" + end + end end # Return the value that should be dumped for the command_line option. @@ -490,8 +596,17 @@ def version_string_to_number(version) end end + # The set of options that are understood by the parsing APIs. Note that + # raise_error is not listed here because it is deleted from the options + # hash by raise_error_format_type before the options are dumped. + DUMP_OPTIONS_KEYS = [:command_line, :encoding, :filepath, :freeze, :frozen_string_literal, :line, :main_script, :partial_script, :scopes, :version].freeze + private_constant :DUMP_OPTIONS_KEYS + # Convert the given options into a serialized options string. def dump_options(options) + unknown_keys = options.keys - DUMP_OPTIONS_KEYS + raise ArgumentError, "unknown keyword: #{unknown_keys.first}" unless unknown_keys.empty? + template = +"" values = [] diff --git a/prism.gemspec b/prism.gemspec index c18d68f4d8..c5fa776f83 100644 --- a/prism.gemspec +++ b/prism.gemspec @@ -92,6 +92,7 @@ Gem::Specification.new do |spec| "include/prism/comments.h", "include/prism/constant_pool.h", "include/prism/diagnostic.h", + "include/prism/errors_format.h", "include/prism/excludes.h", "include/prism/integer.h", "include/prism/json.h", @@ -205,6 +206,7 @@ Gem::Specification.new do |spec| "src/constant_pool.c", "src/diagnostic.c", "src/encoding.c", + "src/errors_format.c", "src/integer.c", "src/json.c", "src/line_offset_list.c", diff --git a/rakelib/typecheck.rake b/rakelib/typecheck.rake index 56a216cd9f..ab2d741e06 100644 --- a/rakelib/typecheck.rake +++ b/rakelib/typecheck.rake @@ -213,6 +213,8 @@ namespace :typecheck do case type when RBS::Types::Literal case type.literal + when TrueClass + RBI::Type.simple("TrueClass") when FalseClass RBI::Type.simple("FalseClass") when Symbol diff --git a/rbi/generated/prism.rbi b/rbi/generated/prism.rbi index 8c1b4e99bb..963c2fb851 100644 --- a/rbi/generated/prism.rbi +++ b/rbi/generated/prism.rbi @@ -33,54 +33,54 @@ module Prism VERSION = T.let(nil, String) BACKEND = T.let(nil, Symbol) - sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(ParseResult) } - def self.parse(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(ParseResult) } + def self.parse(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).void } - def self.profile(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).void } + def self.profile(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(LexResult) } - def self.lex(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(LexResult) } + def self.lex(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(ParseLexResult) } - def self.parse_lex(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(ParseLexResult) } + def self.parse_lex(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(String) } - def self.dump(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(String) } + def self.dump(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(T::Array[Comment]) } - def self.parse_comments(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(T::Array[Comment]) } + def self.parse_comments(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(T::Boolean) } - def self.parse_success?(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(T::Boolean) } + def self.parse_success?(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(T::Boolean) } - def self.parse_failure?(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(source: String, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(T::Boolean) } + def self.parse_failure?(source, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(stream: ::T.untyped, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(ParseResult) } - def self.parse_stream(stream, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(stream: ::T.untyped, filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(ParseResult) } + def self.parse_stream(stream, filepath: T.unsafe(nil), command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(ParseResult) } - def self.parse_file(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(ParseResult) } + def self.parse_file(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).void } - def self.profile_file(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).void } + def self.profile_file(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(LexResult) } - def self.lex_file(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(LexResult) } + def self.lex_file(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(ParseLexResult) } - def self.parse_lex_file(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(ParseLexResult) } + def self.parse_lex_file(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(String) } - def self.dump_file(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(String) } + def self.dump_file(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(T::Array[Comment]) } - def self.parse_file_comments(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(T::Array[Comment]) } + def self.parse_file_comments(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(T::Boolean) } - def self.parse_file_success?(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(T::Boolean) } + def self.parse_file_success?(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end - sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, scopes: T::Array[T::Array[Symbol]], version: String).returns(T::Boolean) } - def self.parse_file_failure?(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end + sig { params(filepath: String, command_line: String, encoding: ::T.any(Encoding, FalseClass), freeze: T::Boolean, frozen_string_literal: T::Boolean, line: Integer, main_script: T::Boolean, partial_script: T::Boolean, raise_error: ::T.any(Symbol, TrueClass), scopes: T::Array[T::Array[Symbol]], version: String).returns(T::Boolean) } + def self.parse_file_failure?(filepath, command_line: T.unsafe(nil), encoding: T.unsafe(nil), freeze: T.unsafe(nil), frozen_string_literal: T.unsafe(nil), line: T.unsafe(nil), main_script: T.unsafe(nil), partial_script: T.unsafe(nil), raise_error: T.unsafe(nil), scopes: T.unsafe(nil), version: T.unsafe(nil)); end end diff --git a/sig/generated/prism.rbs b/sig/generated/prism.rbs index 93fbba28eb..e9f4ca2b85 100644 --- a/sig/generated/prism.rbs +++ b/sig/generated/prism.rbs @@ -48,37 +48,37 @@ module Prism def gets: (?Integer integer) -> (String | nil) end - def self.parse: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult + def self.parse: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult - def self.profile: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> void + def self.profile: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> void - def self.lex: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> LexResult + def self.lex: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> LexResult - def self.parse_lex: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseLexResult + def self.parse_lex: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseLexResult - def self.dump: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> String + def self.dump: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> String - def self.parse_comments: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> Array[Comment] + def self.parse_comments: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> Array[Comment] - def self.parse_success?: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool + def self.parse_success?: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> bool - def self.parse_failure?: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool + def self.parse_failure?: (String source, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> bool - def self.parse_stream: (_Stream stream, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult + def self.parse_stream: (_Stream stream, ?filepath: String, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult - def self.parse_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult + def self.parse_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseResult - def self.profile_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> void + def self.profile_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> void - def self.lex_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> LexResult + def self.lex_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> LexResult - def self.parse_lex_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseLexResult + def self.parse_lex_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> ParseLexResult - def self.dump_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> String + def self.dump_file: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> String - def self.parse_file_comments: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> Array[Comment] + def self.parse_file_comments: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> Array[Comment] - def self.parse_file_success?: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool + def self.parse_file_success?: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> bool - def self.parse_file_failure?: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?scopes: Array[Array[Symbol]], ?version: String) -> bool + def self.parse_file_failure?: (String filepath, ?command_line: String, ?encoding: Encoding | false, ?freeze: bool, ?frozen_string_literal: bool, ?line: Integer, ?main_script: bool, ?partial_script: bool, ?raise_error: Symbol | true, ?scopes: Array[Array[Symbol]], ?version: String) -> bool end diff --git a/src/errors_format.c b/src/errors_format.c new file mode 100644 index 0000000000..32956a613a --- /dev/null +++ b/src/errors_format.c @@ -0,0 +1,519 @@ +#include "prism/errors_format.h" + +#include "prism/internal/allocator.h" +#include "prism/internal/buffer.h" +#include "prism/internal/diagnostic.h" +#include "prism/internal/encoding.h" +#include "prism/internal/line_offset_list.h" +#include "prism/internal/parser.h" + +#include +#include +#include +#include +#include + +/** + * Check if the source touched by the given location is valid UTF-8. The + * location represents the location of the error, but the slice of the source + * that will be displayed includes the content of all of the lines that the + * error touches, so we need to check those parts as well. + */ +static bool +location_is_utf8(const pm_parser_t *parser, const pm_location_t *location) { + const pm_line_offset_list_t *line_offsets = &parser->line_offsets; + const size_t start_line = (size_t) pm_line_offset_list_line_column(line_offsets, location->start, 1).line; + const size_t end_line = (size_t) pm_line_offset_list_line_column(line_offsets, location->start + location->length, 1).line; + + const uint8_t *cursor = parser->start + line_offsets->offsets[start_line - 1]; + const uint8_t *end = (end_line == line_offsets->size) ? parser->end : (parser->start + line_offsets->offsets[end_line]); + + while (cursor < end) { + size_t width = pm_encoding_utf_8_char_width(cursor, end - cursor); + if (width == 0) return false; + + cursor += width; + } + + return true; +} + +typedef struct { + pm_diagnostic_t *diagnostic; + int32_t line; + uint32_t column_start; + uint32_t column_end; + size_t idx; +} pm_rich_error_t; + +static int +pm_rich_error_compare(const void *a, const void *b) { + const pm_rich_error_t *left = (const pm_rich_error_t *) a; + const pm_rich_error_t *right = (const pm_rich_error_t *) b; + + if (left->line != right->line) { + return (left->line < right->line) ? -1 : 1; + } + + if (left->column_start != right->column_start) { + return (left->column_start < right->column_start) ? -1 : 1; + } + + if (left->idx != right->idx) { + return (left->idx < right->idx) ? 1 : -1; + } + + return 0; +} + +#define COLOR_BOLD "\033[1m" +#define COLOR_GRAY "\033[2m" +#define COLOR_RED "\033[1;31m" +#define COLOR_RESET "\033[m" +#define TRUNCATE 30 + +typedef struct { + const char *number_prefix; + const char *blank_prefix; + const char *divider; + size_t blank_prefix_length; + size_t divider_length; +} pm_error_line_format_t; + +/** + * Initialize the given line format struct based on whether or not to highlight + * the line and the first and last line numbers. + */ +static void +pm_error_line_format_init(pm_error_line_format_t *format, pm_errors_format_type_t format_type, int32_t first_line, int32_t last_line) { + /* If we have a maximum line number that is negative, then we are going to + * use the absolute value for comparison but multiply by 10 to additionally + * have a column for the negative sign. */ + if (first_line < 0) first_line = (-first_line) * 10; + if (last_line < 0) last_line = (-last_line) * 10; + int32_t max_line = first_line > last_line ? first_line : last_line; + + if (max_line < 10) { + if (format_type > PM_ERRORS_FORMAT_PLAIN) { + *format = (pm_error_line_format_t) { + .number_prefix = COLOR_GRAY "%1" PRIi32 " | " COLOR_RESET, + .blank_prefix = COLOR_GRAY " | " COLOR_RESET, + .divider = COLOR_GRAY " ~~~~~" COLOR_RESET "\n" + }; + } else { + *format = (pm_error_line_format_t) { + .number_prefix = "%1" PRIi32 " | ", + .blank_prefix = " | ", + .divider = " ~~~~~\n" + }; + } + } else if (max_line < 100) { + if (format_type > PM_ERRORS_FORMAT_PLAIN) { + *format = (pm_error_line_format_t) { + .number_prefix = COLOR_GRAY "%2" PRIi32 " | " COLOR_RESET, + .blank_prefix = COLOR_GRAY " | " COLOR_RESET, + .divider = COLOR_GRAY " ~~~~~~" COLOR_RESET "\n" + }; + } else { + *format = (pm_error_line_format_t) { + .number_prefix = "%2" PRIi32 " | ", + .blank_prefix = " | ", + .divider = " ~~~~~~\n" + }; + } + } else if (max_line < 1000) { + if (format_type > PM_ERRORS_FORMAT_PLAIN) { + *format = (pm_error_line_format_t) { + .number_prefix = COLOR_GRAY "%3" PRIi32 " | " COLOR_RESET, + .blank_prefix = COLOR_GRAY " | " COLOR_RESET, + .divider = COLOR_GRAY " ~~~~~~~" COLOR_RESET "\n" + }; + } else { + *format = (pm_error_line_format_t) { + .number_prefix = "%3" PRIi32 " | ", + .blank_prefix = " | ", + .divider = " ~~~~~~~\n" + }; + } + } else if (max_line < 10000) { + if (format_type > PM_ERRORS_FORMAT_PLAIN) { + *format = (pm_error_line_format_t) { + .number_prefix = COLOR_GRAY "%4" PRIi32 " | " COLOR_RESET, + .blank_prefix = COLOR_GRAY " | " COLOR_RESET, + .divider = COLOR_GRAY " ~~~~~~~~" COLOR_RESET "\n" + }; + } else { + *format = (pm_error_line_format_t) { + .number_prefix = "%4" PRIi32 " | ", + .blank_prefix = " | ", + .divider = " ~~~~~~~~\n" + }; + } + } else { + if (format_type > PM_ERRORS_FORMAT_PLAIN) { + *format = (pm_error_line_format_t) { + .number_prefix = COLOR_GRAY "%5" PRIi32 " | " COLOR_RESET, + .blank_prefix = COLOR_GRAY " | " COLOR_RESET, + .divider = COLOR_GRAY " ~~~~~~~~" COLOR_RESET "\n" + }; + } else { + *format = (pm_error_line_format_t) { + .number_prefix = "%5" PRIi32 " | ", + .blank_prefix = " | ", + .divider = " ~~~~~~~~\n" + }; + } + } + + format->blank_prefix_length = strlen(format->blank_prefix); + format->divider_length = strlen(format->divider); +} + +static void +pm_error_format_line(const pm_parser_t *parser, pm_buffer_t *buffer, const char *number_prefix, int32_t line, uint32_t column_start, uint32_t column_end) { + int32_t line_delta = line - parser->start_line; + assert(line_delta >= 0); + + size_t index = (size_t) line_delta; + assert(index < parser->line_offsets.size); + + const uint8_t *start = parser->start + parser->line_offsets.offsets[index]; + const uint8_t *end; + + if (index >= parser->line_offsets.size - 1) { + end = parser->end; + } else { + end = parser->start + parser->line_offsets.offsets[index + 1]; + } + + pm_buffer_append_format(buffer, number_prefix, line); + + /* Here we determine if we should truncate the end of the line. Note that + * this is written to avoid computing start + column_end, which could be + * more than one past the end of the source when the error is at the end + * of the input. */ + bool truncate_end = false; + if ((column_end != 0) && ((end - start) - ((ptrdiff_t) column_end) >= TRUNCATE)) { + const uint8_t *end_candidate = start + column_end + TRUNCATE; + + for (const uint8_t *cursor = start; cursor < end_candidate;) { + size_t char_width = pm_parser_encoding_char_width(parser, cursor, parser->end - cursor); + + /* If we failed to decode a character, then just bail out and + * truncate at the fixed width. */ + if (char_width == 0) break; + + /* If this next character would go past the end candidate, + * then we need to truncate before it. */ + if (cursor + char_width > end_candidate) { + end_candidate = cursor; + break; + } + + cursor += char_width; + } + + end = end_candidate; + truncate_end = true; + } + + /* Here we determine if we should truncate the start of the line. */ + if (column_start >= TRUNCATE) { + pm_buffer_append_string(buffer, "... ", 4); + start += column_start; + } + + pm_buffer_append_string(buffer, (const char *) start, (size_t) (end - start)); + + if (truncate_end) { + pm_buffer_append_string(buffer, " ...\n", 5); + } else if (end == parser->end && end > parser->start && end[-1] != '\n') { + pm_buffer_append_byte(buffer, '\n'); + } +} + +static void +pm_rich_errors_format(const pm_parser_t *parser, pm_buffer_t *buffer, pm_errors_format_type_t format_type, size_t nerrors, pm_rich_error_t *rich_errors, bool inline_messages) { + pm_error_line_format_t format; + pm_error_line_format_init(&format, format_type, rich_errors[0].line, rich_errors[nerrors - 1].line); + + /* We are going to iterate through every error in our error list and display + * it. While we are iterating, we will display some padding lines of the + * source before the error to give some context. We will be careful not to + * display the same line twice in case the errors are close enough in the + * source. */ + int32_t last_line = parser->start_line - 1; + uint32_t last_column_start = 0; + + for (size_t idx = 0; idx < nerrors; idx++) { + pm_rich_error_t *rich_error = rich_errors + idx; + + /* Here we determine how many lines of padding of the source to display, + * based on the difference from the last line that was displayed. */ + if (rich_error->line - last_line > 1) { + if (rich_error->line - last_line > 2) { + if ((idx != 0) && (rich_error->line - last_line > 3)) { + pm_buffer_append_string(buffer, format.divider, format.divider_length); + } + + pm_buffer_append_string(buffer, " ", 2); + pm_error_format_line(parser, buffer, format.number_prefix, rich_error->line - 2, 0, 0); + } + + pm_buffer_append_string(buffer, " ", 2); + pm_error_format_line(parser, buffer, format.number_prefix, rich_error->line - 1, 0, 0); + } + + /* If this is the first error or we are on a new line, then we will + * display the line that has the error in it. */ + if ((idx == 0) || (rich_error->line != last_line)) { + switch (format_type) { + case PM_ERRORS_FORMAT_PLAIN: + pm_buffer_append_string(buffer, "> ", 2); + break; + case PM_ERRORS_FORMAT_STYLE: + pm_buffer_append_format(buffer, "%s", COLOR_BOLD "> " COLOR_RESET); + break; + case PM_ERRORS_FORMAT_COLOR: + pm_buffer_append_format(buffer, "%s", COLOR_RED "> " COLOR_RESET); + break; + } + + last_column_start = rich_error->column_start; + + /* Find the maximum column end of all the errors on this line. */ + uint32_t column_end = rich_error->column_end; + for (size_t next_idx = idx + 1; next_idx < nerrors; next_idx++) { + if (rich_errors[next_idx].line != rich_error->line) break; + if (rich_errors[next_idx].column_end > column_end) column_end = rich_errors[next_idx].column_end; + } + + pm_error_format_line(parser, buffer, format.number_prefix, rich_error->line, rich_error->column_start, column_end); + } + + const uint8_t *start = parser->start + parser->line_offsets.offsets[rich_error->line - parser->start_line]; + if (start == parser->end) pm_buffer_append_byte(buffer, '\n'); + + /* Now we will display the actual error message. We will do this by + * first putting the prefix to the line, then a bunch of blank spaces + * depending on the column, then as many tildes as we need to display + * the width of the error, then the error message itself. + * + * Note that this doesn't take into account the width of the actual + * character when displayed in the terminal. For some east-asian + * languages or emoji, this means it can be thrown off pretty badly. We + * will need to solve this eventually. */ + pm_buffer_append_string(buffer, " ", 2); + pm_buffer_append_string(buffer, format.blank_prefix, format.blank_prefix_length); + + size_t column = 0; + if (last_column_start >= TRUNCATE) { + pm_buffer_append_string(buffer, " ", 4); + column = last_column_start; + } + + while (column < rich_error->column_start) { + pm_buffer_append_byte(buffer, ' '); + + size_t char_width = pm_parser_encoding_char_width(parser, start + column, parser->end - (start + column)); + column += (char_width == 0 ? 1 : char_width); + } + + switch (format_type) { + case PM_ERRORS_FORMAT_PLAIN: + pm_buffer_append_byte(buffer, '^'); + break; + case PM_ERRORS_FORMAT_STYLE: + pm_buffer_append_format(buffer, "%s^", COLOR_BOLD); + break; + case PM_ERRORS_FORMAT_COLOR: + pm_buffer_append_format(buffer, "%s^", COLOR_RED); + break; + } + + size_t char_width = pm_parser_encoding_char_width(parser, start + column, parser->end - (start + column)); + column += (char_width == 0 ? 1 : char_width); + + while (column < rich_error->column_end) { + pm_buffer_append_byte(buffer, '~'); + + size_t char_width = pm_parser_encoding_char_width(parser, start + column, parser->end - (start + column)); + column += (char_width == 0 ? 1 : char_width); + } + + if (format_type != PM_ERRORS_FORMAT_PLAIN) { + pm_buffer_append_format(buffer, "%s", COLOR_RESET); + } + + if (inline_messages) { + const char *message = pm_diagnostic_message(rich_error->diagnostic); + + pm_buffer_append_byte(buffer, ' '); + pm_buffer_append_string(buffer, message, strlen(message)); + } + + pm_buffer_append_byte(buffer, '\n'); + + /* Here we determine how many lines of padding to display after the + * diagnostic, depending on where the next diagnostic is in source. */ + last_line = rich_error->line; + int32_t next_line; + + if (idx == nerrors - 1) { + next_line = ((int32_t) parser->line_offsets.size) + parser->start_line; + + /* If the file ends with a newline, subtract one from our + * "next_line" so that we do not output an extra line at the end of + * the file. */ + if ((parser->start + parser->line_offsets.offsets[parser->line_offsets.size - 1]) == parser->end) { + next_line--; + } + } else { + next_line = rich_errors[idx + 1].line; + } + + if (next_line - last_line > 1) { + pm_buffer_append_string(buffer, " ", 2); + pm_error_format_line(parser, buffer, format.number_prefix, ++last_line, 0, 0); + } + + if (next_line - last_line > 1) { + pm_buffer_append_string(buffer, " ", 2); + pm_error_format_line(parser, buffer, format.number_prefix, ++last_line, 0, 0); + } + } +} + +static void +pm_rich_error_init(const pm_parser_t *parser, pm_rich_error_t *rich_error, pm_diagnostic_t *diagnostic, pm_location_t *location) { + const pm_line_offset_list_t *line_offsets = &parser->line_offsets; + pm_line_column_t start_line_column = pm_line_offset_list_line_column(line_offsets, location->start, parser->start_line); + pm_line_column_t end_line_column = pm_line_offset_list_line_column(line_offsets, location->start + location->length, parser->start_line); + + uint32_t column_end; + if (start_line_column.line == end_line_column.line) { + column_end = end_line_column.column; + } else { + column_end = (uint32_t) (line_offsets->offsets[start_line_column.line - parser->start_line + 1] - line_offsets->offsets[start_line_column.line - parser->start_line] - 1); + } + + /* Ensure we have at least one column of error. */ + if (start_line_column.column == column_end) column_end++; + + *rich_error = (pm_rich_error_t) { + .diagnostic = diagnostic, + .line = start_line_column.line, + .column_start = start_line_column.column, + .column_end = column_end + }; +} + +pm_error_level_t +pm_errors_format(const pm_parser_t *parser, pm_buffer_t *buffer, pm_errors_format_type_t format_type) { + size_t nerrors = parser->error_list.size; + assert(nerrors > 0); + + int filepath_length = (int) pm_string_length(&parser->filepath); + const char *filepath = (const char *) pm_string_source(&parser->filepath); + + size_t rich_errors_size = nerrors * sizeof(pm_rich_error_t); + if (rich_errors_size / sizeof(pm_rich_error_t) != nerrors) abort(); + + pm_rich_error_t *rich_errors = xmalloc(rich_errors_size); + if (rich_errors == NULL) abort(); + + bool is_utf8 = true; + size_t rich_errors_idx = 0; + + for (pm_diagnostic_t *diagnostic = (pm_diagnostic_t *) parser->error_list.head; diagnostic != NULL; diagnostic = (pm_diagnostic_t *) diagnostic->node.next) { + pm_location_t location = pm_diagnostic_location(diagnostic); + + switch (pm_diagnostic_error_level(diagnostic)) { + case PM_ERROR_LEVEL_SYNTAX: { + if (is_utf8 && !location_is_utf8(parser, &location)) { + is_utf8 = false; + } + + pm_rich_error_init(parser, &rich_errors[rich_errors_idx], diagnostic, &location); + rich_errors[rich_errors_idx].idx = rich_errors_idx; + rich_errors_idx++; + break; + } + case PM_ERROR_LEVEL_ARGUMENT: { + /* If we get an argument error, we want to format it and return it + * immediately. They should include a snippet of the source if + * possible. */ + pm_buffer_append_format( + buffer, + "%.*s:%" PRIi32 ": %s", + filepath_length, + filepath, + pm_line_offset_list_line(&parser->line_offsets, location.start, parser->start_line), + pm_diagnostic_message(diagnostic) + ); + + if (location_is_utf8(parser, &location)) { + pm_buffer_append_byte(buffer, '\n'); + pm_rich_error_init(parser, rich_errors, diagnostic, &location); + pm_rich_errors_format(parser, buffer, format_type, 1, rich_errors, false); + } + + xfree_sized(rich_errors, rich_errors_size); + return PM_ERROR_LEVEL_ARGUMENT; + } + case PM_ERROR_LEVEL_LOAD: { + /* If we get a load error, we want to format it and return it + * immediately. It should only include the diagnostic message. */ + const char *message = pm_diagnostic_message(diagnostic); + pm_buffer_append_string(buffer, message, strlen(message)); + + xfree_sized(rich_errors, rich_errors_size); + return PM_ERROR_LEVEL_LOAD; + } + } + } + + assert(rich_errors_idx == nerrors); + + /* The header displays the line of the first error in parse order, which + * is the first entry in the array because it has not been sorted yet. */ + pm_buffer_append_format( + buffer, + "%.*s:%" PRIi32 ": syntax error%s found\n", + filepath_length, + filepath, + rich_errors[0].line, + (nerrors > 1) ? "s" : "" + ); + + if (is_utf8) { + /* In here, we have all of the rich diagnostic information and we know + * the snippets are valid UTF-8. We will sort the errors so that they + * are displayed in the order that they appear in the source, and then + * format them all together with the source code snippets. */ + qsort(rich_errors, nerrors, sizeof(pm_rich_error_t), pm_rich_error_compare); + pm_rich_errors_format(parser, buffer, format_type, nerrors, rich_errors, true); + } else { + /* In here, we have content in the source that is not valid UTF-8. In + * that case, we do not want to attempt to format the source code + * snippets because we do not know how the terminal will handle it. The + * diagnostics are displayed in the order that they were generated, + * which is the order of the array before it has been sorted. */ + for (size_t idx = 0; idx < nerrors; idx++) { + pm_rich_error_t *rich_error = &rich_errors[idx]; + + if (idx > 0) pm_buffer_append_byte(buffer, '\n'); + pm_buffer_append_format( + buffer, + "%.*s:%" PRIi32 ": %s", + filepath_length, + filepath, + rich_error->line, + pm_diagnostic_message(rich_error->diagnostic) + ); + } + } + + xfree_sized(rich_errors, rich_errors_size); + return PM_ERROR_LEVEL_SYNTAX; +} diff --git a/src/options.c b/src/options.c index b589865a2a..9e9cbf7915 100644 --- a/src/options.c +++ b/src/options.c @@ -243,6 +243,20 @@ pm_options_freeze_set(pm_options_t *options, bool freeze) { options->freeze = freeze; } +/** + * Get the raise_error option on the given options struct. + */ +uint8_t pm_options_raise_error(const pm_options_t *options) { + return options->raise_error; +} + +/** + * Set the raise_error option on the given options struct. + */ +void pm_options_raise_error_set(pm_options_t *options, uint8_t raise_error) { + options->raise_error = raise_error; +} + // For some reason, GCC analyzer thinks we're leaking allocated scopes and // locals here, even though we definitely aren't. This is a false positive. // Ideally we wouldn't need to suppress this. diff --git a/src/prism.c b/src/prism.c index 511223aaee..9ec1d33fda 100644 --- a/src/prism.c +++ b/src/prism.c @@ -23375,6 +23375,41 @@ pm_serialize_parse_stream(pm_buffer_t *buffer, pm_source_t *source, const char * pm_options_cleanup(&options); } +/** + * Parse the given source and format any errors that are encountered into the + * given buffer using the given format type. If the source parses without any + * errors, then -1 is returned and the buffer is left empty. Otherwise, the + * name of the encoding of the source is written to the buffer, followed by a + * null byte, followed by the formatted errors, and the error level of the + * error with the highest precedence is returned. + */ +int8_t +pm_serialize_parse_errors_format(pm_buffer_t *buffer, const uint8_t *source, size_t size, const char *data, pm_errors_format_type_t format_type) { + pm_options_t options = { 0 }; + pm_options_read(&options, data); + + pm_arena_t arena = { 0 }; + pm_parser_t parser; + pm_parser_init(&arena, &parser, source, size, &options); + + pm_parse(&parser); + + int8_t result = -1; + if (parser.error_list.size > 0) { + const char *encoding_name = parser.encoding->name; + pm_buffer_append_string(buffer, encoding_name, strlen(encoding_name)); + pm_buffer_append_byte(buffer, '\0'); + + result = (int8_t) pm_errors_format(&parser, buffer, format_type); + } + + pm_parser_cleanup(&parser); + pm_arena_cleanup(&arena); + pm_options_cleanup(&options); + + return result; +} + /** * Parse and serialize the comments in the given source to the given buffer. */ diff --git a/test/prism/api/raise_error_test.rb b/test/prism/api/raise_error_test.rb new file mode 100644 index 0000000000..6ca12fa5e2 --- /dev/null +++ b/test/prism/api/raise_error_test.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module Prism + class RaiseErrorTest < TestCase + def test_option_not_set + assert_nothing_raised { Prism.parse("foo = 1 +") } + assert_nothing_raised { Prism.parse("foo = 1 +", raise_error: nil) } + end + + def test_option_invalid + error = assert_raise(ArgumentError) { Prism.parse("foo = 1", raise_error: :bogus) } + assert_includes error.message, "invalid raise_error value: bogus" + + error = assert_raise(TypeError) { Prism.parse("foo = 1", raise_error: "plain") } + assert_equal "wrong argument type String (expected Symbol)", error.message + end + + def test_option_misspelled + error = assert_raise(ArgumentError) { Prism.parse("foo = 1 +", raise_errors: :plain) } + assert_includes error.message, "unknown keyword" + end + + def test_syntax_error_single + error = assert_raise(SyntaxError) { Prism.parse("foo(\n", raise_error: :plain, filepath: "single.rb") } + assert_includes error.message, "syntax error found" + assert_syntax_error_path "single.rb", error + end + + def test_syntax_error_multiple + error = assert_raise(SyntaxError) { Prism.parse("foo = 1 +\n", raise_error: :plain, filepath: "test.rb") } + assert_includes error.message, "syntax errors found" + end + + def test_syntax_error_true + # The formatting is determined by whether or not $stderr is a terminal + # and the NO_COLOR environment variable, so only assert that it raises. + assert_raise(SyntaxError) { Prism.parse("foo = 1 +\n", raise_error: true) } + end + + def test_syntax_error_style + error = assert_raise(SyntaxError) { Prism.parse("foo = 1 +\n", raise_error: :style) } + assert_include error.message, "\e[m" + assert_not_include error.message, "\e[1;31m" + end + + def test_syntax_error_color + error = assert_raise(SyntaxError) { Prism.parse("foo = 1 +\n", raise_error: :color) } + assert_include error.message, "\e[1;31m" + end + + def test_syntax_error_not_utf8 + error = assert_raise(SyntaxError) { Prism.parse("foo = 1 + \xFF\xFE\n".b, raise_error: :plain, filepath: "bin.rb") } + assert_include error.message, "invalid multibyte character" + assert_not_include error.message, "> 1 |" + end + + def test_argument_error + assert_raise(ArgumentError) { Prism.parse("# encoding: bogus\n1 + 1\n", raise_error: :plain, filepath: "enc.rb") } + end + + def test_load_error + error = assert_raise(LoadError) { Prism.parse("foo = 1\n", raise_error: :plain, command_line: "x") } + assert_nil error.path + end + + def test_string_apis + source = "foo = 1 +\n" + apis = [ + -> { Prism.parse(source, raise_error: :plain) }, + -> { Prism.lex(source, raise_error: :plain) }, + -> { Prism.parse_lex(source, raise_error: :plain) }, + -> { Prism.parse_comments(source, raise_error: :plain) }, + -> { Prism.profile(source, raise_error: :plain) }, + -> { Prism.parse_success?(source, raise_error: :plain) }, + -> { Prism.parse_failure?(source, raise_error: :plain) }, + -> { Prism.parse_stream(StringIO.new(source), raise_error: :plain) } + ] + + if !ENV["PRISM_BUILD_MINIMAL"] + apis << -> { Prism.dump(source, raise_error: :plain) } + end + + apis.each do |api| + error = assert_raise(SyntaxError, &api) + assert_syntax_error_path "", error + end + end + + def test_file_apis + Tempfile.create(["raise_error", ".rb"]) do |file| + file.write("foo = 1 +\n") + file.close + + filepath = file.path + apis = [ + -> { Prism.parse_file(filepath, raise_error: :plain) }, + -> { Prism.lex_file(filepath, raise_error: :plain) }, + -> { Prism.parse_lex_file(filepath, raise_error: :plain) }, + -> { Prism.parse_file_comments(filepath, raise_error: :plain) }, + -> { Prism.profile_file(filepath, raise_error: :plain) }, + -> { Prism.parse_file_success?(filepath, raise_error: :plain) }, + -> { Prism.parse_file_failure?(filepath, raise_error: :plain) } + ] + + if !ENV["PRISM_BUILD_MINIMAL"] + apis << -> { Prism.dump_file(filepath, raise_error: :plain) } + end + + apis.each do |api| + error = assert_raise(SyntaxError, &api) + assert_syntax_error_path filepath, error + end + end + end + + def test_valid_source + source = "foo = 1\n" + + Tempfile.create(["raise_error", ".rb"]) do |file| + file.write(source) + file.close + + assert_nothing_raised do + Prism.parse(source, raise_error: :plain) + Prism.lex(source, raise_error: :plain) + Prism.parse_lex(source, raise_error: :plain) + Prism.parse_comments(source, raise_error: :plain) + Prism.profile(source, raise_error: :plain) + Prism.parse_success?(source, raise_error: :plain) + Prism.parse_stream(StringIO.new(source), raise_error: :plain) + Prism.parse_file(file.path, raise_error: :plain) + + if !ENV["PRISM_BUILD_MINIMAL"] + Prism.dump(source, raise_error: :plain) + end + end + end + end + + private + + if RUBY_ENGINE == "truffleruby" + def assert_syntax_error_path(expected, error) + end + elsif SyntaxError.method_defined?(:path) + def assert_syntax_error_path(expected, error) + assert_equal expected, error.path + end + else + def assert_syntax_error_path(expected, error) + assert_equal expected, error.instance_variable_get(:@path) + end + end + end +end diff --git a/test/prism/newline_test.rb b/test/prism/newline_test.rb index ffaadf952c..0accd8af6d 100644 --- a/test/prism/newline_test.rb +++ b/test/prism/newline_test.rb @@ -13,6 +13,7 @@ class NewlineTest < TestCase test_helper.rb unescape_test.rb api/parse_stream_test.rb + api/raise_error_test.rb encoding/regular_expression_encoding_test.rb encoding/string_encoding_test.rb result/breadth_first_search_test.rb