diff --git a/ext/prism/extension.c b/ext/prism/extension.c index 83415d0c29..0c9d04225d 100644 --- a/ext/prism/extension.c +++ b/ext/prism/extension.c @@ -25,6 +25,7 @@ VALUE rb_cPrismLexResult; VALUE rb_cPrismParseLexResult; VALUE rb_cPrismStringQuery; VALUE rb_cPrismScope; +VALUE rb_cPrismCurrentVersionError; VALUE rb_cPrismDebugEncoding; @@ -199,7 +200,13 @@ build_options_i(VALUE key, VALUE value, VALUE argument) { if (!NIL_P(value)) { const char *version = check_string(value); - if (!pm_options_version_set(options, version, RSTRING_LEN(value))) { + if (RSTRING_LEN(value) == 7 && strncmp(version, "current", 7) == 0) { + VALUE current_ruby_value = rb_const_get(rb_cObject, rb_intern("RUBY_VERSION")); + const char *current_version = RSTRING_PTR(current_ruby_value); + if (!pm_options_version_set(options, current_version, 3)) { + rb_exc_raise(rb_exc_new_str(rb_cPrismCurrentVersionError, current_ruby_value)); + } + } else if (!pm_options_version_set(options, version, RSTRING_LEN(value))) { rb_raise(rb_eArgError, "invalid version: %" PRIsVALUE, value); } } @@ -888,7 +895,7 @@ parse_input(pm_string_t *input, const pm_options_t *options) { * version of Ruby syntax (which you can trigger with `nil` or * `"latest"`). You may also restrict the syntax to a specific version of * Ruby, e.g., with `"3.3.0"`. To parse with the same syntax version that - * the current Ruby is running use `version: RUBY_VERSION`. Raises + * the current Ruby is running use `version: "current"`. Raises * ArgumentError if the version is not currently supported by Prism. */ static VALUE @@ -1364,6 +1371,8 @@ Init_prism(void) { rb_cPrismStringQuery = rb_define_class_under(rb_cPrism, "StringQuery", rb_cObject); rb_cPrismScope = rb_define_class_under(rb_cPrism, "Scope", rb_cObject); + 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. rb_id_option_command_line = rb_intern_const("command_line"); diff --git a/lib/prism.rb b/lib/prism.rb index dceba4b1f5..2cbe196b57 100644 --- a/lib/prism.rb +++ b/lib/prism.rb @@ -37,6 +37,27 @@ module Prism private_constant :LexCompat private_constant :LexRipper + # Raised when requested to parse as the currently running Ruby version but Prism has no support for it. + class CurrentVersionError < ArgumentError + # Initialize a new exception for the given ruby version string. + def initialize(version) + message = +"invalid version: Requested to parse as `version: 'current'`; " + gem_version = + begin + Gem::Version.new(version) + rescue ArgumentError + end + + if gem_version && gem_version < Gem::Version.new("3.3.0") + message << " #{version} is below the minimum supported syntax." + else + message << " #{version} is unknown. Please update the `prism` gem." + end + + super(message) + end + end + # :call-seq: # Prism::lex_compat(source, **options) -> LexCompat::Result # diff --git a/lib/prism/ffi.rb b/lib/prism/ffi.rb index 5ae177055f..a3bf5786bd 100644 --- a/lib/prism/ffi.rb +++ b/lib/prism/ffi.rb @@ -423,7 +423,9 @@ def dump_options_command_line(options) # Return the value that should be dumped for the version option. def dump_options_version(version) - case version + current = version == "current" + + case current ? RUBY_VERSION : version when nil, "latest" 0 # Handled in pm_parser_init when /\A3\.3(\.\d+)?\z/ @@ -433,7 +435,11 @@ def dump_options_version(version) when /\A3\.5(\.\d+)?\z/ 3 else - raise ArgumentError, "invalid version: #{version}" + if current + raise CurrentVersionError, RUBY_VERSION + else + raise ArgumentError, "invalid version: #{version}" + end end end diff --git a/templates/sig/prism.rbs.erb b/templates/sig/prism.rbs.erb index 2f30cbc29f..5c74cee8f8 100644 --- a/templates/sig/prism.rbs.erb +++ b/templates/sig/prism.rbs.erb @@ -2,6 +2,10 @@ module Prism BACKEND: :CEXT | :FFI VERSION: String + class CurrentVersionError < ArgumentError + def initialize: (String version) -> void + end + # Methods taking a Ruby source code string: <%- { diff --git a/test/prism/api/parse_test.rb b/test/prism/api/parse_test.rb index bbce8a8fad..67a252c589 100644 --- a/test/prism/api/parse_test.rb +++ b/test/prism/api/parse_test.rb @@ -140,6 +140,25 @@ def test_version end end + def test_version_current + if RUBY_VERSION >= "3.3" + assert Prism.parse_success?("1 + 1", version: "current") + else + assert_raise(CurrentVersionError) { Prism.parse_success?("1 + 1", version: "current") } + end + + version = RUBY_VERSION.split(".").tap { |segments| segments[0] = segments[0].succ }.join(".") + stub_ruby_version(version) do + error = assert_raise(CurrentVersionError) { Prism.parse("1 + 1", version: "current") } + assert_includes error.message, "unknown" + end + + stub_ruby_version("2.7.0") do + error = assert_raise(CurrentVersionError) { Prism.parse("1 + 1", version: "current") } + assert_includes error.message, "minimum" + end + end + def test_scopes assert_kind_of Prism::CallNode, Prism.parse_statement("foo") assert_kind_of Prism::LocalVariableReadNode, Prism.parse_statement("foo", scopes: [[:foo]]) @@ -167,5 +186,16 @@ def find_source_file_node(program) queue.concat(node.compact_child_nodes) end end + + def stub_ruby_version(version) + old_version = RUBY_VERSION + + Object.send(:remove_const, :RUBY_VERSION) + Object.const_set(:RUBY_VERSION, version) + yield + ensure + Object.send(:remove_const, :RUBY_VERSION) + Object.const_set(:RUBY_VERSION, old_version) + end end end