[ruby/prism] Port diagnostics format to Prism from CRuby

https://github.com/ruby/prism/commit/46a5f19f77
This commit is contained in:
Kevin Newton committed 2026-07-24 17:08:34 -04:00
1 parent 08fb9346de
commit 2526cb6154
14 files changed
+1237 -123
Notes: git 2026-07-24 21:09:08 +00:00

No files matched your search

+17 -17
View File
@@ -103,23 +103,23 @@ 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.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"
+121 -6
View File
@@ -93,6 +93,7 @@ module Prism # :nodoc:
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 @@ module Prism # :nodoc:
"pm_serialize_lex",
"pm_serialize_parse_lex",
"pm_serialize_parse_success_p",
"pm_serialize_parse_errors_format",
[]
)
@@ -293,6 +295,8 @@ module Prism # :nodoc:
# 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 @@ module Prism # :nodoc:
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 @@ module Prism # :nodoc:
# 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 @@ module Prism # :nodoc:
# 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 @@ module Prism # :nodoc:
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 @@ module Prism # :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 @@ module Prism # :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 @@ module Prism # :nodoc:
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 = []
+2
View File
@@ -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",
+519
View File
@@ -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 <assert.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
/**
* 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;
}
+53
View File
@@ -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
+270 -100
View File
@@ -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();
}
+12
View File
@@ -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
+14
View File
@@ -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.
+16
View File
@@ -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.
*
+35
View File
@@ -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.
*/
+1
View File
@@ -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"
+19
View File
@@ -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
+157
View File
@@ -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
+1
View File
@@ -13,6 +13,7 @@ module Prism
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