From 43654b30fabaabdec1ade8297b10fe2504ca12f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 12:24:07 +0000 Subject: [PATCH 1/2] Gate forwarding parameter syntax behind a parser option `(...)` forwarding parameters (#3042) are syntax-only for now: nothing defines their type checking semantics yet, and distributing signatures that use them would break older parsers and tools that silently drop the node. Introduce `rbs_parser_options_t` and `rbs_parser_new_with_options()` so the syntax must be opted into at the C API level. `rbs_parser_new()` uses the zero-initialized options, which disable every optional syntax, and the Ruby API doesn't expose the option, so `(...)` is a syntax error everywhere in the gem. The AST, types, and serialization support stays in place for when the semantics are settled. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015bgoC4byczqmYRzNrDkVrL --- include/rbs/parser.h | 27 ++++++++++++++++++ src/parser.c | 11 ++++++++ test/rbs/method_type_parsing_test.rb | 41 ++++++++++++---------------- test/rbs/schema_test.rb | 10 ++++++- test/rbs/wasm/serialization_test.rb | 1 - 5 files changed, 65 insertions(+), 25 deletions(-) diff --git a/include/rbs/parser.h b/include/rbs/parser.h index 2ae53922be..060592d045 100644 --- a/include/rbs/parser.h +++ b/include/rbs/parser.h @@ -40,6 +40,21 @@ typedef struct rbs_error_t { bool syntax_error; } rbs_error_t; +/** + * Options that control which syntax the parser accepts. + * + * Zero-initializing the struct gives the default configuration, where + * every optional syntax is disabled. + * */ +typedef struct { + /** + * Accept `(...)` forwarding parameters in method types. + * + * The syntax is experimental and disabled by default. + * */ + bool enable_forwarding_params; +} rbs_parser_options_t; + /** * An RBS parser is a LL(3) parser. * */ @@ -57,6 +72,8 @@ typedef struct { rbs_constant_pool_t constant_pool; rbs_allocator_t *allocator; rbs_error_t *error; + + rbs_parser_options_t options; } rbs_parser_t; /** @@ -107,6 +124,16 @@ RBS_NODISCARD rbs_lexer_t *rbs_lexer_new(rbs_allocator_t *, rbs_string_t string, * Returns `NULL` for a `start_pos` that `rbs_lexer_new` rejects. * */ RBS_NODISCARD rbs_parser_t *rbs_parser_new(rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos); + +/** + * Allocate new rbs_parser_t object with the given options. + * + * `rbs_parser_new` is equivalent to passing a zero-initialized + * `rbs_parser_options_t`, which disables every optional syntax. + * + * Returns `NULL` for a `start_pos` that `rbs_lexer_new` rejects. + * */ +RBS_NODISCARD rbs_parser_t *rbs_parser_new_with_options(rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos, rbs_parser_options_t options); void rbs_parser_free(rbs_parser_t *parser); /** diff --git a/src/parser.c b/src/parser.c index 1690a22205..5effb219e4 100644 --- a/src/parser.c +++ b/src/parser.c @@ -556,6 +556,11 @@ static bool parse_params(rbs_parser_t *parser, method_params *params, bool forwa return false; } + if (!parser->options.enable_forwarding_params) { + rbs_parser_set_error(parser, parser->next_token, true, "forwarding parameter syntax is not enabled"); + return false; + } + rbs_parser_advance(parser); params->forwarding = (rbs_node_t *) rbs_types_function_forwarding_param_new( ALLOCATOR(), @@ -3569,6 +3574,10 @@ rbs_lexer_t *rbs_lexer_new(rbs_allocator_t *allocator, rbs_string_t string, cons } rbs_parser_t *rbs_parser_new(rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos) { + return rbs_parser_new_with_options(string, encoding, start_pos, end_pos, (rbs_parser_options_t) { 0 }); +} + +rbs_parser_t *rbs_parser_new_with_options(rbs_string_t string, const rbs_encoding_t *encoding, int start_pos, int end_pos, rbs_parser_options_t options) { rbs_allocator_t *allocator = rbs_allocator_init(); rbs_lexer_t *lexer = rbs_lexer_new(allocator, string, encoding, start_pos, end_pos); @@ -3593,6 +3602,8 @@ rbs_parser_t *rbs_parser_new(rbs_string_t string, const rbs_encoding_t *encoding .constant_pool = { 0 }, .allocator = allocator, .error = NULL, + + .options = options, }; // The parser's constant pool is mainly used for storing the names of type variables, which usually aren't many. diff --git a/test/rbs/method_type_parsing_test.rb b/test/rbs/method_type_parsing_test.rb index cfb982c3bf..9d191d4dfe 100644 --- a/test/rbs/method_type_parsing_test.rb +++ b/test/rbs/method_type_parsing_test.rb @@ -48,34 +48,29 @@ def test_method_param end end - def test_forwarding_parameter - parse_method_type("(...) -> void").tap do |type| - assert_equal "(...) -> void", type.to_s - assert_instance_of Types::Function::ForwardingParam, type.type.forwarding - assert_equal "...", type.type.forwarding.location.source - assert_empty type.type.required_positionals - assert_nil type.block + def test_forwarding_parameter_syntax_is_not_enabled + # `(...)` forwarding parameters are gated behind a C-level parser option + # (`rbs_parser_options_t`) that the Ruby API doesn't expose, so parsing + # them from Ruby is always an error. + error = assert_raise(RBS::ParsingError) do + parse_method_type("(...) -> void") end + assert_include error.message, "forwarding parameter syntax is not enabled" - parse_method_type("(String message, ...) -> void").tap do |type| - assert_equal "(String message, ...) -> void", type.to_s - assert_equal 1, type.type.required_positionals.size - assert_predicate type.type, :forwarding? - assert_instance_of Types::Function::ForwardingParam, type.type.forwarding + assert_raise(RBS::ParsingError) do + parse_method_type("(String message, ...) -> void") end end - def test_forwarding_parameter_with_overload_continuation - _, _, declarations = parse_signature(<<~RBS) - class Foo - def foo: (...) -> void - | ... - end - RBS - - method = declarations.fetch(0).members.fetch(0) - assert_predicate method, :overloading? - assert_predicate method.overloads.fetch(0).method_type.type, :forwarding? + def test_forwarding_parameter_syntax_is_not_enabled_in_signature + assert_raise(RBS::ParsingError) do + parse_signature(<<~RBS) + class Foo + def foo: (...) -> void + | ... + end + RBS + end end def test_forwarding_parameter_rejects_nonleading_parameters diff --git a/test/rbs/schema_test.rb b/test/rbs/schema_test.rb index a76e707c8f..c6b5814ff3 100644 --- a/test/rbs/schema_test.rb +++ b/test/rbs/schema_test.rb @@ -121,8 +121,16 @@ def test_method_type_schema parse_method_type("[G] (A a, ?B, *C, d: D, ?e: E e, **f) ?{ (G) -> void } -> String").to_json ) + # Forwarding parameters can't be parsed from Ruby, so build the node directly JSONValidator.method_type.validate!( - parse_method_type("(String message, ...) -> void").to_json + RBS::MethodType.new( + type_params: [], + type: RBS::Types::Function.empty(RBS::Types::Bases::Void.new(location: nil)).update( + forwarding: RBS::Types::Function::ForwardingParam.new(location: nil) + ), + block: nil, + location: nil + ).to_json ) end diff --git a/test/rbs/wasm/serialization_test.rb b/test/rbs/wasm/serialization_test.rb index cfc8278902..ccd4b99a0f 100644 --- a/test/rbs/wasm/serialization_test.rb +++ b/test/rbs/wasm/serialization_test.rb @@ -109,7 +109,6 @@ def test_method_type_round_trip "(Integer) -> String", "[T] (T) -> T", "(Integer, ?String, *Symbol, foo: bool, ?bar: Integer, **untyped) -> void", - "(String message, ...) -> void", "() { (Integer) -> void } -> bool", "() ?{ () -> void } -> void", "[A, B < Comparable[A]] (A) -> B", From 78e4d68379951b6c2207eb88ed9aabbd23be5197 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 13:05:04 +0000 Subject: [PATCH 2/2] Let the private parser entry points enable forwarding parameters Gating `(...)` behind a C-level option left the enabled path with no test coverage from Ruby, where the whole suite lives. Without it the forwarding branch of `parse_params`, its AST translation, and its serialization would be free to rot until the semantics are settled. Thread the option through the private `_parse_method_type` and `_parse_signature` entry points (and their `_to_bytes` variants), which tests already call directly. The public `RBS::Parser` API keeps passing `false`, so signatures written for the gem still can't use the syntax. This also strengthens the restriction tests: they used to pass simply because the gate rejected `(...)` outright, and now run with the option enabled, so they check the grammar restrictions they name. The WebAssembly parser has no way to enable the option, so its shim raises `NotImplementedError` rather than quietly parsing without it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015bgoC4byczqmYRzNrDkVrL --- ext/rbs_extension/main.c | 44 ++++++++++++------ lib/rbs/parser_aux.rb | 4 +- lib/rbs/wasm/parser.rb | 15 +++++- sig/parser.rbs | 8 ++-- test/rbs/method_type_parsing_test.rb | 69 ++++++++++++++++++++++------ test/rbs/parser_test.rb | 8 ++-- test/rbs/schema_test.rb | 12 ++--- test/rbs/wasm/serialization_test.rb | 9 ++-- 8 files changed, 117 insertions(+), 52 deletions(-) diff --git a/ext/rbs_extension/main.c b/ext/rbs_extension/main.c index 06434640ce..2d2a40f6ba 100644 --- a/ext/rbs_extension/main.c +++ b/ext/rbs_extension/main.c @@ -185,7 +185,16 @@ static rbs_lexer_t *alloc_lexer_from_buffer(rbs_allocator_t *allocator, VALUE st return lexer; } -static rbs_parser_t *alloc_parser_from_buffer(VALUE buffer, int start_pos, int end_pos) { +// Build the parser options from the arguments the `_parse_*` entry points +// receive. The optional syntax these enable is not part of the public +// `RBS::Parser` API, so only the private entry points pass them through. +static rbs_parser_options_t parser_options(VALUE enable_forwarding_params) { + return (rbs_parser_options_t) { + .enable_forwarding_params = RB_TEST(enable_forwarding_params), + }; +} + +static rbs_parser_t *alloc_parser_from_buffer_with_options(VALUE buffer, int start_pos, int end_pos, rbs_parser_options_t options) { VALUE string = rb_funcall(buffer, rb_intern("content"), 0); StringValue(string); @@ -194,11 +203,12 @@ static rbs_parser_t *alloc_parser_from_buffer(VALUE buffer, int start_pos, int e rb_encoding *encoding = rb_enc_get(string); const char *encoding_name = rb_enc_name(encoding); - rbs_parser_t *parser = rbs_parser_new( + rbs_parser_t *parser = rbs_parser_new_with_options( rbs_string_from_ruby_string(string), rbs_encoding_find((const uint8_t *) encoding_name, (const uint8_t *) (encoding_name + strlen(encoding_name))), start_pos, - end_pos + end_pos, + options ); if (parser == NULL) { @@ -208,6 +218,10 @@ static rbs_parser_t *alloc_parser_from_buffer(VALUE buffer, int start_pos, int e return parser; } +static rbs_parser_t *alloc_parser_from_buffer(VALUE buffer, int start_pos, int end_pos) { + return alloc_parser_from_buffer_with_options(buffer, start_pos, end_pos, (rbs_parser_options_t) { 0 }); +} + static VALUE rbsparser_parse_type(VALUE self, VALUE buffer, VALUE start_pos, VALUE end_pos, VALUE variables, VALUE require_eof, VALUE void_allowed, VALUE self_allowed, VALUE classish_allowed) { VALUE string = rb_funcall(buffer, rb_intern("content"), 0); StringValue(string); @@ -254,12 +268,12 @@ static VALUE parse_method_type_try(VALUE a) { return rbs_struct_to_ruby_value(ctx, (rbs_node_t *) method_type); } -static VALUE rbsparser_parse_method_type(VALUE self, VALUE buffer, VALUE start_pos, VALUE end_pos, VALUE variables, VALUE require_eof) { +static VALUE rbsparser_parse_method_type(VALUE self, VALUE buffer, VALUE start_pos, VALUE end_pos, VALUE variables, VALUE require_eof, VALUE enable_forwarding_params) { VALUE string = rb_funcall(buffer, rb_intern("content"), 0); StringValue(string); rb_encoding *encoding = rb_enc_get(string); - rbs_parser_t *parser = alloc_parser_from_buffer(buffer, FIX2INT(start_pos), FIX2INT(end_pos)); + rbs_parser_t *parser = alloc_parser_from_buffer_with_options(buffer, FIX2INT(start_pos), FIX2INT(end_pos), parser_options(enable_forwarding_params)); declare_type_variables(parser, variables, buffer); struct parse_method_type_arg arg = { .buffer = buffer, @@ -293,12 +307,12 @@ static VALUE parse_signature_try(VALUE a) { return rbs_struct_to_ruby_value(ctx, (rbs_node_t *) signature); } -static VALUE rbsparser_parse_signature(VALUE self, VALUE buffer, VALUE start_pos, VALUE end_pos) { +static VALUE rbsparser_parse_signature(VALUE self, VALUE buffer, VALUE start_pos, VALUE end_pos, VALUE enable_forwarding_params) { VALUE string = rb_funcall(buffer, rb_intern("content"), 0); StringValue(string); rb_encoding *encoding = rb_enc_get(string); - rbs_parser_t *parser = alloc_parser_from_buffer(buffer, FIX2INT(start_pos), FIX2INT(end_pos)); + rbs_parser_t *parser = alloc_parser_from_buffer_with_options(buffer, FIX2INT(start_pos), FIX2INT(end_pos), parser_options(enable_forwarding_params)); struct parse_signature_arg arg = { .buffer = buffer, .encoding = encoding, @@ -386,12 +400,12 @@ static VALUE parse_method_type_to_bytes_try(VALUE a) { return serialized_node_to_string(parser, (rbs_node_t *) method_type); } -static VALUE rbsparser_parse_method_type_to_bytes(VALUE self, VALUE buffer, VALUE start_pos, VALUE end_pos, VALUE variables, VALUE require_eof) { +static VALUE rbsparser_parse_method_type_to_bytes(VALUE self, VALUE buffer, VALUE start_pos, VALUE end_pos, VALUE variables, VALUE require_eof, VALUE enable_forwarding_params) { VALUE string = rb_funcall(buffer, rb_intern("content"), 0); StringValue(string); rb_encoding *encoding = rb_enc_get(string); - rbs_parser_t *parser = alloc_parser_from_buffer(buffer, FIX2INT(start_pos), FIX2INT(end_pos)); + rbs_parser_t *parser = alloc_parser_from_buffer_with_options(buffer, FIX2INT(start_pos), FIX2INT(end_pos), parser_options(enable_forwarding_params)); declare_type_variables(parser, variables, buffer); struct parse_method_type_arg arg = { .buffer = buffer, @@ -419,12 +433,12 @@ static VALUE parse_signature_to_bytes_try(VALUE a) { return serialized_node_to_string(parser, (rbs_node_t *) signature); } -static VALUE rbsparser_parse_signature_to_bytes(VALUE self, VALUE buffer, VALUE start_pos, VALUE end_pos) { +static VALUE rbsparser_parse_signature_to_bytes(VALUE self, VALUE buffer, VALUE start_pos, VALUE end_pos, VALUE enable_forwarding_params) { VALUE string = rb_funcall(buffer, rb_intern("content"), 0); StringValue(string); rb_encoding *encoding = rb_enc_get(string); - rbs_parser_t *parser = alloc_parser_from_buffer(buffer, FIX2INT(start_pos), FIX2INT(end_pos)); + rbs_parser_t *parser = alloc_parser_from_buffer_with_options(buffer, FIX2INT(start_pos), FIX2INT(end_pos), parser_options(enable_forwarding_params)); struct parse_signature_arg arg = { .buffer = buffer, .encoding = encoding, @@ -609,11 +623,11 @@ void rbs__init_parser(void) { rb_gc_register_mark_object(EMPTY_HASH); rb_define_singleton_method(RBS_Parser, "_parse_type", rbsparser_parse_type, 8); - rb_define_singleton_method(RBS_Parser, "_parse_method_type", rbsparser_parse_method_type, 5); - rb_define_singleton_method(RBS_Parser, "_parse_signature", rbsparser_parse_signature, 3); + rb_define_singleton_method(RBS_Parser, "_parse_method_type", rbsparser_parse_method_type, 6); + rb_define_singleton_method(RBS_Parser, "_parse_signature", rbsparser_parse_signature, 4); rb_define_singleton_method(RBS_Parser, "_parse_type_to_bytes", rbsparser_parse_type_to_bytes, 8); - rb_define_singleton_method(RBS_Parser, "_parse_method_type_to_bytes", rbsparser_parse_method_type_to_bytes, 5); - rb_define_singleton_method(RBS_Parser, "_parse_signature_to_bytes", rbsparser_parse_signature_to_bytes, 3); + rb_define_singleton_method(RBS_Parser, "_parse_method_type_to_bytes", rbsparser_parse_method_type_to_bytes, 6); + rb_define_singleton_method(RBS_Parser, "_parse_signature_to_bytes", rbsparser_parse_signature_to_bytes, 4); rb_define_singleton_method(RBS_Parser, "_parse_type_params", rbsparser_parse_type_params, 4); rb_define_singleton_method(RBS_Parser, "_parse_inline_leading_annotation", rbsparser_parse_inline_leading_annotation, 4); rb_define_singleton_method(RBS_Parser, "_parse_inline_trailing_annotation", rbsparser_parse_inline_trailing_annotation, 4); diff --git a/lib/rbs/parser_aux.rb b/lib/rbs/parser_aux.rb index ff14f951d7..0cb81f32c4 100644 --- a/lib/rbs/parser_aux.rb +++ b/lib/rbs/parser_aux.rb @@ -14,7 +14,7 @@ def self.parse_type(source, range: nil, byte_range: 0..., variables: [], require def self.parse_method_type(source, range: nil, byte_range: 0..., variables: [], require_eof: false) buf = buffer(source) byte_range = byte_range(range, buf.content) if range - _parse_method_type(buf, byte_range.begin || 0, byte_range.end || buf.content.bytesize, variables, require_eof) + _parse_method_type(buf, byte_range.begin || 0, byte_range.end || buf.content.bytesize, variables, require_eof, false) end def self.parse_signature(source) @@ -28,7 +28,7 @@ def self.parse_signature(source) 0 end content = buf.content - dirs, decls = _parse_signature(buf, start_pos, content.bytesize) + dirs, decls = _parse_signature(buf, start_pos, content.bytesize, false) if resolved dirs = dirs.dup if dirs.frozen? diff --git a/lib/rbs/wasm/parser.rb b/lib/rbs/wasm/parser.rb index 9f111150aa..338e95d01b 100644 --- a/lib/rbs/wasm/parser.rb +++ b/lib/rbs/wasm/parser.rb @@ -12,8 +12,9 @@ module RBS # RBS::Parser API on top, exactly as it does for the C extension. class Parser class << self - def _parse_signature(buffer, start_pos, end_pos) + def _parse_signature(buffer, start_pos, end_pos, enable_forwarding_params) validate_position_range(buffer, start_pos, end_pos) + validate_parser_options(enable_forwarding_params) encoding = buffer.content.encoding.name status, bytes = WASM::Runtime.instance.parse_signature(buffer.content, encoding, start_pos, end_pos) raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK @@ -31,9 +32,10 @@ def _parse_type(buffer, start_pos, end_pos, variables, require_eof, void_allowed deserialize_or_nil(bytes, buffer) end - def _parse_method_type(buffer, start_pos, end_pos, variables, require_eof) + def _parse_method_type(buffer, start_pos, end_pos, variables, require_eof, enable_forwarding_params) validate_position_range(buffer, start_pos, end_pos) validate_variables(variables) + validate_parser_options(enable_forwarding_params) encoding = buffer.content.encoding.name status, bytes = WASM::Runtime.instance.parse_method_type(buffer.content, encoding, start_pos, end_pos, variables, require_eof) raise_parse_failure(buffer, status, bytes, start_pos, end_pos) unless status == WASM::Runtime::OK @@ -99,6 +101,15 @@ def validate_position_range(buffer, start_pos, end_pos) end end + # The WebAssembly entry points (rbs_wasm.c) build their parsers with the + # default options, so the optional syntax the C extension can enable is + # not reachable here. The public RBS::Parser API never enables it. + def validate_parser_options(enable_forwarding_params) + if enable_forwarding_params + raise NotImplementedError, "forwarding parameter syntax is not supported by the WebAssembly parser" + end + end + # Reject anything that is not nil or an Array of Symbols, matching # declare_type_variables in the C extension (main.c). def validate_variables(variables) diff --git a/sig/parser.rbs b/sig/parser.rbs index c10b08152a..facf74300b 100644 --- a/sig/parser.rbs +++ b/sig/parser.rbs @@ -136,9 +136,9 @@ module RBS def self._parse_type: (Buffer, Integer start_pos, Integer end_pos, Array[Symbol] variables, bool require_eof, bool void_allowed, bool self_allowed, bool classish_allowed) -> Types::t? - def self._parse_method_type: (Buffer, Integer start_pos, Integer end_pos, Array[Symbol] variables, bool require_eof) -> MethodType? + def self._parse_method_type: (Buffer, Integer start_pos, Integer end_pos, Array[Symbol] variables, bool require_eof, bool enable_forwarding_params) -> MethodType? - def self._parse_signature: (Buffer, Integer start_pos, Integer end_pos) -> [Array[AST::Directives::t], Array[AST::Declarations::t]] + def self._parse_signature: (Buffer, Integer start_pos, Integer end_pos, bool enable_forwarding_params) -> [Array[AST::Directives::t], Array[AST::Declarations::t]] # Parse and serialize the result to the binary format consumed by # RBS::WASM::Deserializer (see ext/rbs_extension/main.c and @@ -146,9 +146,9 @@ module RBS # round-trip can be exercised on CRuby. def self._parse_type_to_bytes: (Buffer, Integer start_pos, Integer end_pos, Array[Symbol] variables, bool require_eof, bool void_allowed, bool self_allowed, bool classish_allowed) -> String? - def self._parse_method_type_to_bytes: (Buffer, Integer start_pos, Integer end_pos, Array[Symbol] variables, bool require_eof) -> String? + def self._parse_method_type_to_bytes: (Buffer, Integer start_pos, Integer end_pos, Array[Symbol] variables, bool require_eof, bool enable_forwarding_params) -> String? - def self._parse_signature_to_bytes: (Buffer, Integer start_pos, Integer end_pos) -> String + def self._parse_signature_to_bytes: (Buffer, Integer start_pos, Integer end_pos, bool enable_forwarding_params) -> String def self._parse_type_params: (Buffer, Integer start_pos, Integer end_pos, bool module_type_params) -> Array[AST::TypeParam] diff --git a/test/rbs/method_type_parsing_test.rb b/test/rbs/method_type_parsing_test.rb index 9d191d4dfe..59b60abc12 100644 --- a/test/rbs/method_type_parsing_test.rb +++ b/test/rbs/method_type_parsing_test.rb @@ -23,6 +23,19 @@ def parse_signature(string) RBS::Parser.parse_signature(buffer) end + # `(...)` forwarding parameters are gated behind a parser option that the + # public API deliberately doesn't expose, so the tests below reach for the + # private entry points to exercise the syntax itself. + def parse_method_type_with_forwarding(string) + buffer = Buffer.new(content: string.encode(Encoding::UTF_8), name: "sample.rbs") + RBS::Parser._parse_method_type(buffer, 0, buffer.content.bytesize, nil, true, true) + end + + def parse_signature_with_forwarding(string) + buffer = Buffer.new(content: string.encode(Encoding::UTF_8), name: "sample.rbs") + RBS::Parser._parse_signature(buffer, 0, buffer.content.bytesize, true) + end + def test_method_type Parser.parse_method_type("()->void").yield_self do |type| assert_equal "() -> void", type.to_s @@ -48,10 +61,9 @@ def test_method_param end end - def test_forwarding_parameter_syntax_is_not_enabled - # `(...)` forwarding parameters are gated behind a C-level parser option - # (`rbs_parser_options_t`) that the Ruby API doesn't expose, so parsing - # them from Ruby is always an error. + def test_forwarding_parameter_syntax_is_not_enabled_by_default + # The syntax has no type checking semantics yet, so the public API never + # enables it. Signatures shipped in the wild can't use it. error = assert_raise(RBS::ParsingError) do parse_method_type("(...) -> void") end @@ -60,19 +72,46 @@ def test_forwarding_parameter_syntax_is_not_enabled assert_raise(RBS::ParsingError) do parse_method_type("(String message, ...) -> void") end - end - def test_forwarding_parameter_syntax_is_not_enabled_in_signature assert_raise(RBS::ParsingError) do parse_signature(<<~RBS) class Foo def foo: (...) -> void - | ... end RBS end end + def test_forwarding_parameter + parse_method_type_with_forwarding("(...) -> void").tap do |type| + assert_equal "(...) -> void", type.to_s + assert_instance_of Types::Function::ForwardingParam, type.type.forwarding + assert_equal "...", type.type.forwarding.location.source + assert_empty type.type.required_positionals + assert_nil type.block + end + + parse_method_type_with_forwarding("(String message, ...) -> void").tap do |type| + assert_equal "(String message, ...) -> void", type.to_s + assert_equal 1, type.type.required_positionals.size + assert_predicate type.type, :forwarding? + assert_instance_of Types::Function::ForwardingParam, type.type.forwarding + end + end + + def test_forwarding_parameter_with_overload_continuation + _, declarations = parse_signature_with_forwarding(<<~RBS) + class Foo + def foo: (...) -> void + | ... + end + RBS + + method = declarations.fetch(0).members.fetch(0) + assert_predicate method, :overloading? + assert_predicate method.overloads.fetch(0).method_type.type, :forwarding? + end + def test_forwarding_parameter_rejects_nonleading_parameters [ "(?String value, ...) -> void", @@ -82,7 +121,7 @@ def test_forwarding_parameter_rejects_nonleading_parameters "(**String values, ...) -> void", ].each do |source| assert_raise(RBS::ParsingError) do - parse_method_type(source) + parse_method_type_with_forwarding(source) end end end @@ -94,7 +133,7 @@ def test_forwarding_parameter_must_be_last "(...,) -> void", ].each do |source| assert_raise(RBS::ParsingError) do - parse_method_type(source) + parse_method_type_with_forwarding(source) end end end @@ -105,25 +144,29 @@ def test_forwarding_parameter_cannot_have_explicit_block "(...) ?{ () -> void } -> void", ].each do |source| assert_raise(RBS::ParsingError) do - parse_method_type(source) + parse_method_type_with_forwarding(source) end end end def test_forwarding_parameter_is_not_allowed_in_block_types - assert_raise(RBS::ParsingError) do - parse_method_type("() { (...) -> void } -> void") + error = assert_raise(RBS::ParsingError) do + parse_method_type_with_forwarding("() { (...) -> void } -> void") end + assert_include error.message, "forwarding parameter is not allowed in this context" end def test_forwarding_parameter_is_not_allowed_in_proc_types + # Proc types are parsed by `_parse_type`, which has no way to enable the + # syntax, but the context restriction is checked before the option anyway. [ "^(...) -> void", "^(String, ...) -> void", ].each do |source| - assert_raise(RBS::ParsingError) do + error = assert_raise(RBS::ParsingError) do parse_type(source) end + assert_include error.message, "forwarding parameter is not allowed in this context" end end diff --git a/test/rbs/parser_test.rb b/test/rbs/parser_test.rb index f0e61e7e84..bf7d1ed647 100644 --- a/test/rbs/parser_test.rb +++ b/test/rbs/parser_test.rb @@ -1033,7 +1033,7 @@ class Foo[T < Integer] < Bar # Comment def test_invalid_position_range_raises # Regression: start_pos > end_pos used to cause an infinite loop in the lexer. assert_raises(ArgumentError) do - RBS::Parser._parse_signature(buffer(""), 1, 0) + RBS::Parser._parse_signature(buffer(""), 1, 0, false) end end @@ -1051,7 +1051,7 @@ def test_invalid_utf8_byte_in_comment_does_not_hang # Regression: invalid UTF-8 byte in a comment used to loop forever in the lexer. source = "# \xC2".dup.force_encoding(Encoding::UTF_8) assert_raises(RBS::ParsingError) do - RBS::Parser._parse_signature(buffer(source), 0, source.bytesize) + RBS::Parser._parse_signature(buffer(source), 0, source.bytesize, false) end end @@ -1061,7 +1061,7 @@ def test_invalid_utf8_byte_at_top_level_raises # Regression: invalid UTF-8 byte at top level used to trip RBS_ASSERT in the C extension. source = "\xFF".dup.force_encoding(Encoding::UTF_8) assert_raises(RBS::ParsingError) do - RBS::Parser._parse_signature(buffer(source), 0, source.bytesize) + RBS::Parser._parse_signature(buffer(source), 0, source.bytesize, false) end end @@ -1183,7 +1183,7 @@ def test_utf8_replacement_character_in_comment_parses # ("\xEF\xBF\xBD") that decodes to the multibyte dummy code point, not the # sentinel that marks an invalid byte. A comment containing it must parse fine. source = "# \u{FFFD}\ntype x = untyped\n".dup.force_encoding(Encoding::UTF_8) - _, decls = RBS::Parser._parse_signature(buffer(source), 0, source.bytesize) + _, decls = RBS::Parser._parse_signature(buffer(source), 0, source.bytesize, false) assert_equal 1, decls.size assert_instance_of RBS::AST::Declarations::TypeAlias, decls[0] end diff --git a/test/rbs/schema_test.rb b/test/rbs/schema_test.rb index c6b5814ff3..aedf9ad6a7 100644 --- a/test/rbs/schema_test.rb +++ b/test/rbs/schema_test.rb @@ -121,15 +121,11 @@ def test_method_type_schema parse_method_type("[G] (A a, ?B, *C, d: D, ?e: E e, **f) ?{ (G) -> void } -> String").to_json ) - # Forwarding parameters can't be parsed from Ruby, so build the node directly + # Forwarding parameters are only parsed when explicitly enabled + source = "(String message, ...) -> void" JSONValidator.method_type.validate!( - RBS::MethodType.new( - type_params: [], - type: RBS::Types::Function.empty(RBS::Types::Bases::Void.new(location: nil)).update( - forwarding: RBS::Types::Function::ForwardingParam.new(location: nil) - ), - block: nil, - location: nil + RBS::Parser._parse_method_type( + RBS::Buffer.new(content: source, name: "test.rbs"), 0, source.bytesize, nil, true, true ).to_json ) end diff --git a/test/rbs/wasm/serialization_test.rb b/test/rbs/wasm/serialization_test.rb index ccd4b99a0f..890e05fbb3 100644 --- a/test/rbs/wasm/serialization_test.rb +++ b/test/rbs/wasm/serialization_test.rb @@ -24,8 +24,8 @@ def buffer(source) end def assert_round_trips(buf) - directives, decls = RBS::Parser._parse_signature(buf, 0, buf.content.bytesize) - bytes = RBS::Parser._parse_signature_to_bytes(buf, 0, buf.content.bytesize) + directives, decls = RBS::Parser._parse_signature(buf, 0, buf.content.bytesize, false) + bytes = RBS::Parser._parse_signature_to_bytes(buf, 0, buf.content.bytesize, false) actual = RBS::WASM::Deserializer.deserialize(bytes, buf) diff = ast_diff([directives, decls], actual) @@ -109,6 +109,7 @@ def test_method_type_round_trip "(Integer) -> String", "[T] (T) -> T", "(Integer, ?String, *Symbol, foo: bool, ?bar: Integer, **untyped) -> void", + "(String message, ...) -> void", "() { (Integer) -> void } -> bool", "() ?{ () -> void } -> void", "[A, B < Comparable[A]] (A) -> B", @@ -116,8 +117,8 @@ def test_method_type_round_trip method_types.each do |source| buf = buffer(source) - expected = RBS::Parser._parse_method_type(buf, 0, source.bytesize, nil, true) - bytes = RBS::Parser._parse_method_type_to_bytes(buf, 0, source.bytesize, nil, true) + expected = RBS::Parser._parse_method_type(buf, 0, source.bytesize, nil, true, true) + bytes = RBS::Parser._parse_method_type_to_bytes(buf, 0, source.bytesize, nil, true, true) actual = RBS::WASM::Deserializer.deserialize(bytes, buf) assert_nil ast_diff(expected, actual), "method type round-trip mismatch for #{source.inspect}"