Skip to content

Commit

Permalink
[ruby/prism] Prism.parse_success?(source)
Browse files Browse the repository at this point in the history
A lot of tools use Ripper/RubyVM::AbstractSyntaxTree to determine
if a source is valid. These tools both create an AST instead of
providing an API that will return a boolean only.

This new API only creates the C structs, but doesn't bother
reifying them into Ruby/the serialization API. Instead it only
returns true/false, which is significantly more efficient.

ruby/prism@7014740118
  • Loading branch information
kddnewton authored and matzbot committed Dec 1, 2023
1 parent b77551a commit 492c82c
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 1 deletion.
16 changes: 16 additions & 0 deletions lib/prism.rb
Expand Up @@ -64,6 +64,22 @@ def self.lex_ripper(source)
def self.load(source, serialized)
Serialize.load(source, serialized)
end

# :call-seq:
# Prism::parse_failure?(source, **options) -> bool
#
# Returns true if the source is invalid Ruby code.
def self.parse_failure?(source, **options)
!parse_success?(source, **options)
end

# :call-seq:
# Prism::parse_file_failure?(filepath, **options) -> bool
#
# Returns true if the file at filepath is invalid Ruby code.
def self.parse_file_failure?(filepath, **options)
!parse_file_success?(filepath, **options)
end
end

require_relative "prism/node"
Expand Down
15 changes: 14 additions & 1 deletion lib/prism/ffi.rb
Expand Up @@ -72,7 +72,8 @@ def self.load_exported_functions_from(header, *functions)
"pm_serialize_parse",
"pm_serialize_parse_comments",
"pm_serialize_lex",
"pm_serialize_parse_lex"
"pm_serialize_parse_lex",
"pm_parse_success_p"
)

load_exported_functions_from(
Expand Down Expand Up @@ -268,6 +269,18 @@ def parse_lex_file(filepath, **options)
end
end

# Mirror the Prism.parse_success? API by using the serialization API.
def parse_success?(code, **options)
LibRubyParser.pm_parse_success_p(code, code.bytesize, dump_options(options))
end

# Mirror the Prism.parse_file_success? API by using the serialization API.
def parse_file_success?(filepath, **options)
LibRubyParser::PrismString.with(filepath) do |string|
parse_success?(string.read, **options, filepath: filepath)
end
end

private

# Convert the given options into a serialized options string.
Expand Down
60 changes: 60 additions & 0 deletions prism/extension.c
Expand Up @@ -798,6 +798,64 @@ parse_lex_file(int argc, VALUE *argv, VALUE self) {
return value;
}

/**
* Parse the given input and return true if it parses without errors or
* warnings.
*/
static VALUE
parse_input_success_p(pm_string_t *input, const pm_options_t *options) {
pm_parser_t parser;
pm_parser_init(&parser, pm_string_source(input), pm_string_length(input), options);

pm_node_t *node = pm_parse(&parser);
pm_node_destroy(&parser, node);

VALUE result = parser.error_list.size == 0 && parser.warning_list.size == 0 ? Qtrue : Qfalse;
pm_parser_free(&parser);

return result;
}

/**
* call-seq:
* Prism::parse_success?(source, **options) -> Array
*
* Parse the given string and return true if it parses without errors or
* warnings. For supported options, see Prism::parse.
*/
static VALUE
parse_success_p(int argc, VALUE *argv, VALUE self) {
pm_string_t input;
pm_options_t options = { 0 };
string_options(argc, argv, &input, &options);

VALUE result = parse_input_success_p(&input, &options);
pm_string_free(&input);
pm_options_free(&options);

return result;
}

/**
* call-seq:
* Prism::parse_file_success?(filepath, **options) -> Array
*
* Parse the given file and return true if it parses without errors or warnings.
* For supported options, see Prism::parse.
*/
static VALUE
parse_file_success_p(int argc, VALUE *argv, VALUE self) {
pm_string_t input;
pm_options_t options = { 0 };
if (!file_options(argc, argv, &input, &options)) return Qnil;

VALUE result = parse_input_success_p(&input, &options);
pm_string_free(&input);
pm_options_free(&options);

return result;
}

/******************************************************************************/
/* Utility functions exposed to make testing easier */
/******************************************************************************/
Expand Down Expand Up @@ -981,6 +1039,8 @@ Init_prism(void) {
rb_define_singleton_method(rb_cPrism, "parse_file_comments", parse_file_comments, -1);
rb_define_singleton_method(rb_cPrism, "parse_lex", parse_lex, -1);
rb_define_singleton_method(rb_cPrism, "parse_lex_file", parse_lex_file, -1);
rb_define_singleton_method(rb_cPrism, "parse_success?", parse_success_p, -1);
rb_define_singleton_method(rb_cPrism, "parse_file_success?", parse_file_success_p, -1);

// Next, the functions that will be called by the parser to perform various
// internal tasks. We expose these to make them easier to test.
Expand Down
10 changes: 10 additions & 0 deletions prism/prism.h
Expand Up @@ -152,6 +152,16 @@ PRISM_EXPORTED_FUNCTION void pm_serialize_lex(pm_buffer_t *buffer, const uint8_t
*/
PRISM_EXPORTED_FUNCTION void pm_serialize_parse_lex(pm_buffer_t *buffer, const uint8_t *source, size_t size, const char *data);

/**
* Parse the source and return true if it parses without errors or warnings.
*
* @param source The source to parse.
* @param size The size of the source.
* @param data The optional data to pass to the parser.
* @return True if the source parses without errors or warnings.
*/
PRISM_EXPORTED_FUNCTION bool pm_parse_success_p(const uint8_t *source, size_t size, const char *data);

/**
* Returns a string representation of the given token type.
*
Expand Down
21 changes: 21 additions & 0 deletions prism/templates/src/serialize.c.erb
Expand Up @@ -359,3 +359,24 @@ pm_serialize_parse_lex(pm_buffer_t *buffer, const uint8_t *source, size_t size,
pm_parser_free(&parser);
pm_options_free(&options);
}

/**
* Parse the source and return true if it parses without errors or warnings.
*/
PRISM_EXPORTED_FUNCTION bool
pm_parse_success_p(const uint8_t *source, size_t size, const char *data) {
pm_options_t options = { 0 };
pm_options_read(&options, data);

pm_parser_t parser;
pm_parser_init(&parser, source, size, &options);

pm_node_t *node = pm_parse(&parser);
pm_node_destroy(&parser, node);

bool result = parser.error_list.size == 0 && parser.warning_list.size == 0;
pm_parser_free(&parser);
pm_options_free(&options);

return result;
}
12 changes: 12 additions & 0 deletions test/prism/ruby_api_test.rb
Expand Up @@ -20,6 +20,18 @@ def test_ruby_api
assert_equal_nodes ast2, ast3
end

def test_parse_success?
assert Prism.parse_success?("1")
refute Prism.parse_success?("<>")

assert Prism.parse_success?("m //", verbose: false)
refute Prism.parse_success?("m //", verbose: true)
end

def test_parse_file_success?
assert Prism.parse_file_success?(__FILE__)
end

def test_options
assert_equal "", Prism.parse("__FILE__").value.statements.body[0].filepath
assert_equal "foo.rb", Prism.parse("__FILE__", filepath: "foo.rb").value.statements.body[0].filepath
Expand Down

0 comments on commit 492c82c

Please sign in to comment.