diff --git a/config.yml b/config.yml index bf44dde25..f8c0767ac 100644 --- a/config.yml +++ b/config.yml @@ -784,6 +784,20 @@ nodes: - name: type_name_location c_type: rbs_location_range optional: true + - name: RBS::AST::Ruby::Annotations::ParamTypeAnnotation + rust_name: ParamTypeAnnotationNode + fields: + - name: prefix_location + c_type: rbs_location_range + - name: name_location + c_type: rbs_location_range + - name: colon_location + c_type: rbs_location_range + - name: param_type + c_type: rbs_node + - name: comment_location + c_type: rbs_location_range + optional: true enums: attribute_visibility: @@ -815,4 +829,4 @@ enums: symbols: - invariant - covariant - - contravariant \ No newline at end of file + - contravariant diff --git a/docs/inline.md b/docs/inline.md index 47c520773..bc668fc91 100644 --- a/docs/inline.md +++ b/docs/inline.md @@ -219,21 +219,60 @@ end #### Doc-style syntax +The doc-style syntax allows annotating individual method parameters and the return type using `@rbs NAME: TYPE` comments. + +The `@rbs PARAM_NAME: T` syntax declares the type of a parameter: + +```ruby +class Calculator + # @rbs x: Integer + # @rbs y: Integer + def add(x, y:) + x + y + end +end +``` + +You can add a description after `--`: + +```ruby +class Calculator + # @rbs x: Integer -- the first operand + # @rbs y: Integer -- the second operand + def add(x, y:) + x + y + end +end +``` + The `@rbs return: T` syntax declares the return type of a method: ```ruby class Calculator - # @rbs return: String + # @rbs return: String -- a human-readable representation def to_s "Calculator" end end ``` +Both can be combined: + +```ruby +class Calculator + # @rbs x: Integer -- the first operand + # @rbs y: Integer -- the second operand + # @rbs return: Integer + def add(x, y:) + x + y + end +end +``` + ### Current Limitations - Class methods and singleton methods are not supported -- Parameter types are not supported with doc-style syntax +- Only positional and keyword parameters are supported. Splat parameters (`*x`, `**y`) and block parameter (`&block`) are not supported yet. - Method visibility declaration is not supported yet ## Attributes diff --git a/ext/rbs_extension/ast_translation.c b/ext/rbs_extension/ast_translation.c index 3b88cbd2d..949f97c93 100644 --- a/ext/rbs_extension/ast_translation.c +++ b/ext/rbs_extension/ast_translation.c @@ -921,6 +921,23 @@ VALUE rbs_struct_to_ruby_value(rbs_translation_context_t ctx, rbs_node_t *instan &h ); } + case RBS_AST_RUBY_ANNOTATIONS_PARAM_TYPE_ANNOTATION: { + rbs_ast_ruby_annotations_param_type_annotation_t *node = (rbs_ast_ruby_annotations_param_type_annotation_t *) instance; + + VALUE h = rb_hash_new(); + rb_hash_aset(h, ID2SYM(rb_intern("location")), rbs_location_range_to_ruby_location(ctx, node->base.location)); + rb_hash_aset(h, ID2SYM(rb_intern("prefix_location")), rbs_location_range_to_ruby_location(ctx, node->prefix_location)); + rb_hash_aset(h, ID2SYM(rb_intern("name_location")), rbs_location_range_to_ruby_location(ctx, node->name_location)); + rb_hash_aset(h, ID2SYM(rb_intern("colon_location")), rbs_location_range_to_ruby_location(ctx, node->colon_location)); + rb_hash_aset(h, ID2SYM(rb_intern("param_type")), rbs_struct_to_ruby_value(ctx, (rbs_node_t *) node->param_type)); // rbs_node + rb_hash_aset(h, ID2SYM(rb_intern("comment_location")), rbs_location_range_to_ruby_location(ctx, node->comment_location)); // optional + + return CLASS_NEW_INSTANCE( + RBS_AST_Ruby_Annotations_ParamTypeAnnotation, + 1, + &h + ); + } case RBS_AST_RUBY_ANNOTATIONS_RETURN_TYPE_ANNOTATION: { rbs_ast_ruby_annotations_return_type_annotation_t *node = (rbs_ast_ruby_annotations_return_type_annotation_t *) instance; diff --git a/ext/rbs_extension/class_constants.c b/ext/rbs_extension/class_constants.c index f2ed76dcb..1a1e37690 100644 --- a/ext/rbs_extension/class_constants.c +++ b/ext/rbs_extension/class_constants.c @@ -53,6 +53,7 @@ VALUE RBS_AST_Ruby_Annotations_InstanceVariableAnnotation; VALUE RBS_AST_Ruby_Annotations_MethodTypesAnnotation; VALUE RBS_AST_Ruby_Annotations_ModuleAliasAnnotation; VALUE RBS_AST_Ruby_Annotations_NodeTypeAssertion; +VALUE RBS_AST_Ruby_Annotations_ParamTypeAnnotation; VALUE RBS_AST_Ruby_Annotations_ReturnTypeAnnotation; VALUE RBS_AST_Ruby_Annotations_SkipAnnotation; VALUE RBS_AST_Ruby_Annotations_TypeApplicationAnnotation; @@ -142,6 +143,7 @@ void rbs__init_constants(void) { IMPORT_CONSTANT(RBS_AST_Ruby_Annotations_MethodTypesAnnotation, RBS_AST_Ruby_Annotations, "MethodTypesAnnotation"); IMPORT_CONSTANT(RBS_AST_Ruby_Annotations_ModuleAliasAnnotation, RBS_AST_Ruby_Annotations, "ModuleAliasAnnotation"); IMPORT_CONSTANT(RBS_AST_Ruby_Annotations_NodeTypeAssertion, RBS_AST_Ruby_Annotations, "NodeTypeAssertion"); + IMPORT_CONSTANT(RBS_AST_Ruby_Annotations_ParamTypeAnnotation, RBS_AST_Ruby_Annotations, "ParamTypeAnnotation"); IMPORT_CONSTANT(RBS_AST_Ruby_Annotations_ReturnTypeAnnotation, RBS_AST_Ruby_Annotations, "ReturnTypeAnnotation"); IMPORT_CONSTANT(RBS_AST_Ruby_Annotations_SkipAnnotation, RBS_AST_Ruby_Annotations, "SkipAnnotation"); IMPORT_CONSTANT(RBS_AST_Ruby_Annotations_TypeApplicationAnnotation, RBS_AST_Ruby_Annotations, "TypeApplicationAnnotation"); diff --git a/ext/rbs_extension/class_constants.h b/ext/rbs_extension/class_constants.h index e90f22cb5..cdf2e58a3 100644 --- a/ext/rbs_extension/class_constants.h +++ b/ext/rbs_extension/class_constants.h @@ -61,6 +61,7 @@ extern VALUE RBS_AST_Ruby_Annotations_InstanceVariableAnnotation; extern VALUE RBS_AST_Ruby_Annotations_MethodTypesAnnotation; extern VALUE RBS_AST_Ruby_Annotations_ModuleAliasAnnotation; extern VALUE RBS_AST_Ruby_Annotations_NodeTypeAssertion; +extern VALUE RBS_AST_Ruby_Annotations_ParamTypeAnnotation; extern VALUE RBS_AST_Ruby_Annotations_ReturnTypeAnnotation; extern VALUE RBS_AST_Ruby_Annotations_SkipAnnotation; extern VALUE RBS_AST_Ruby_Annotations_TypeApplicationAnnotation; diff --git a/include/rbs/ast.h b/include/rbs/ast.h index 2225dde9e..a19fa791a 100644 --- a/include/rbs/ast.h +++ b/include/rbs/ast.h @@ -96,41 +96,42 @@ enum rbs_node_type { RBS_AST_RUBY_ANNOTATIONS_METHOD_TYPES_ANNOTATION = 35, RBS_AST_RUBY_ANNOTATIONS_MODULE_ALIAS_ANNOTATION = 36, RBS_AST_RUBY_ANNOTATIONS_NODE_TYPE_ASSERTION = 37, - RBS_AST_RUBY_ANNOTATIONS_RETURN_TYPE_ANNOTATION = 38, - RBS_AST_RUBY_ANNOTATIONS_SKIP_ANNOTATION = 39, - RBS_AST_RUBY_ANNOTATIONS_TYPE_APPLICATION_ANNOTATION = 40, - RBS_AST_STRING = 41, - RBS_AST_TYPE_PARAM = 42, - RBS_METHOD_TYPE = 43, - RBS_NAMESPACE = 44, - RBS_SIGNATURE = 45, - RBS_TYPE_NAME = 46, - RBS_TYPES_ALIAS = 47, - RBS_TYPES_BASES_ANY = 48, - RBS_TYPES_BASES_BOOL = 49, - RBS_TYPES_BASES_BOTTOM = 50, - RBS_TYPES_BASES_CLASS = 51, - RBS_TYPES_BASES_INSTANCE = 52, - RBS_TYPES_BASES_NIL = 53, - RBS_TYPES_BASES_SELF = 54, - RBS_TYPES_BASES_TOP = 55, - RBS_TYPES_BASES_VOID = 56, - RBS_TYPES_BLOCK = 57, - RBS_TYPES_CLASS_INSTANCE = 58, - RBS_TYPES_CLASS_SINGLETON = 59, - RBS_TYPES_FUNCTION = 60, - RBS_TYPES_FUNCTION_PARAM = 61, - RBS_TYPES_INTERFACE = 62, - RBS_TYPES_INTERSECTION = 63, - RBS_TYPES_LITERAL = 64, - RBS_TYPES_OPTIONAL = 65, - RBS_TYPES_PROC = 66, - RBS_TYPES_RECORD = 67, - RBS_TYPES_RECORD_FIELD_TYPE = 68, - RBS_TYPES_TUPLE = 69, - RBS_TYPES_UNION = 70, - RBS_TYPES_UNTYPED_FUNCTION = 71, - RBS_TYPES_VARIABLE = 72, + RBS_AST_RUBY_ANNOTATIONS_PARAM_TYPE_ANNOTATION = 38, + RBS_AST_RUBY_ANNOTATIONS_RETURN_TYPE_ANNOTATION = 39, + RBS_AST_RUBY_ANNOTATIONS_SKIP_ANNOTATION = 40, + RBS_AST_RUBY_ANNOTATIONS_TYPE_APPLICATION_ANNOTATION = 41, + RBS_AST_STRING = 42, + RBS_AST_TYPE_PARAM = 43, + RBS_METHOD_TYPE = 44, + RBS_NAMESPACE = 45, + RBS_SIGNATURE = 46, + RBS_TYPE_NAME = 47, + RBS_TYPES_ALIAS = 48, + RBS_TYPES_BASES_ANY = 49, + RBS_TYPES_BASES_BOOL = 50, + RBS_TYPES_BASES_BOTTOM = 51, + RBS_TYPES_BASES_CLASS = 52, + RBS_TYPES_BASES_INSTANCE = 53, + RBS_TYPES_BASES_NIL = 54, + RBS_TYPES_BASES_SELF = 55, + RBS_TYPES_BASES_TOP = 56, + RBS_TYPES_BASES_VOID = 57, + RBS_TYPES_BLOCK = 58, + RBS_TYPES_CLASS_INSTANCE = 59, + RBS_TYPES_CLASS_SINGLETON = 60, + RBS_TYPES_FUNCTION = 61, + RBS_TYPES_FUNCTION_PARAM = 62, + RBS_TYPES_INTERFACE = 63, + RBS_TYPES_INTERSECTION = 64, + RBS_TYPES_LITERAL = 65, + RBS_TYPES_OPTIONAL = 66, + RBS_TYPES_PROC = 67, + RBS_TYPES_RECORD = 68, + RBS_TYPES_RECORD_FIELD_TYPE = 69, + RBS_TYPES_TUPLE = 70, + RBS_TYPES_UNION = 71, + RBS_TYPES_UNTYPED_FUNCTION = 72, + RBS_TYPES_VARIABLE = 73, RBS_AST_SYMBOL, }; @@ -612,6 +613,16 @@ typedef struct rbs_ast_ruby_annotations_node_type_assertion { struct rbs_node *type; } rbs_ast_ruby_annotations_node_type_assertion_t; +typedef struct rbs_ast_ruby_annotations_param_type_annotation { + rbs_node_t base; + + rbs_location_range prefix_location; + rbs_location_range name_location; + rbs_location_range colon_location; + struct rbs_node *param_type; + rbs_location_range comment_location; /* Optional */ +} rbs_ast_ruby_annotations_param_type_annotation_t; + typedef struct rbs_ast_ruby_annotations_return_type_annotation { rbs_node_t base; @@ -881,6 +892,7 @@ typedef union rbs_ast_ruby_annotations { rbs_ast_ruby_annotations_node_type_assertion_t node_type_assertion; rbs_ast_ruby_annotations_return_type_annotation_t return_type_annotation; rbs_ast_ruby_annotations_skip_annotation_t skip_annotation; + rbs_ast_ruby_annotations_param_type_annotation_t param_type_annotation; } rbs_ast_ruby_annotations_t; /// `rbs_ast_symbol_t` models user-defined identifiers like class names, method names, etc. @@ -929,6 +941,7 @@ rbs_ast_ruby_annotations_instance_variable_annotation_t *rbs_ast_ruby_annotation rbs_ast_ruby_annotations_method_types_annotation_t *rbs_ast_ruby_annotations_method_types_annotation_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_location_range prefix_location, rbs_node_list_t *overloads, rbs_location_range_list_t *vertical_bar_locations, rbs_location_range dot3_location); rbs_ast_ruby_annotations_module_alias_annotation_t *rbs_ast_ruby_annotations_module_alias_annotation_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_location_range prefix_location, rbs_location_range keyword_location, rbs_type_name_t *type_name, rbs_location_range type_name_location); rbs_ast_ruby_annotations_node_type_assertion_t *rbs_ast_ruby_annotations_node_type_assertion_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_location_range prefix_location, rbs_node_t *type); +rbs_ast_ruby_annotations_param_type_annotation_t *rbs_ast_ruby_annotations_param_type_annotation_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_location_range prefix_location, rbs_location_range name_location, rbs_location_range colon_location, rbs_node_t *param_type, rbs_location_range comment_location); rbs_ast_ruby_annotations_return_type_annotation_t *rbs_ast_ruby_annotations_return_type_annotation_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_location_range prefix_location, rbs_location_range return_location, rbs_location_range colon_location, rbs_node_t *return_type, rbs_location_range comment_location); rbs_ast_ruby_annotations_skip_annotation_t *rbs_ast_ruby_annotations_skip_annotation_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_location_range prefix_location, rbs_location_range skip_location, rbs_location_range comment_location); rbs_ast_ruby_annotations_type_application_annotation_t *rbs_ast_ruby_annotations_type_application_annotation_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_location_range prefix_location, rbs_node_list_t *type_args, rbs_location_range close_bracket_location, rbs_location_range_list_t *comma_locations); diff --git a/lib/rbs/ast/ruby/annotations.rb b/lib/rbs/ast/ruby/annotations.rb index a8524bcdd..746abae5c 100644 --- a/lib/rbs/ast/ruby/annotations.rb +++ b/lib/rbs/ast/ruby/annotations.rb @@ -252,6 +252,29 @@ def type_fingerprint ] end end + + class ParamTypeAnnotation < Base + attr_reader :name_location, :colon_location, :param_type, :comment_location + + def initialize(location:, prefix_location:, name_location:, colon_location:, param_type:, comment_location:) + super(location, prefix_location) + @name_location = name_location + @colon_location = colon_location + @param_type = param_type + @comment_location = comment_location + end + + def map_type_name(&block) + self.class.new( + location:, + prefix_location:, + name_location: name_location, + colon_location: colon_location, + param_type: param_type.map_type_name { yield _1 }, + comment_location: comment_location + ) #: self + end + end end end end diff --git a/lib/rbs/ast/ruby/members.rb b/lib/rbs/ast/ruby/members.rb index b91300365..c7d7c1ccf 100644 --- a/lib/rbs/ast/ruby/members.rb +++ b/lib/rbs/ast/ruby/members.rb @@ -17,22 +17,30 @@ def initialize(buffer) class MethodTypeAnnotation class DocStyle attr_accessor :return_type_annotation + attr_reader :param_type_annotations def initialize @return_type_annotation = nil + @param_type_annotations = [] end def map_type_name(&block) DocStyle.new.tap do |new| new.return_type_annotation = return_type_annotation&.map_type_name(&block) + new.param_type_annotations.replace( + param_type_annotations.map {|annot| annot.map_type_name(&block) } + ) end #: self end def type_fingerprint - return_type_annotation&.type_fingerprint + [ + return_type_annotation&.type_fingerprint, + param_type_annotations.map(&:type_fingerprint) + ] end - def method_type + def method_type(node) return_type = case return_type_annotation when Annotations::NodeTypeAssertion @@ -43,12 +51,47 @@ def method_type Types::Bases::Any.new(location: nil) end + required_positionals = [] #: Array[Types::Function::Param] + required_keywords = {} #: Hash[Symbol, Types::Function::Param] + + if node.parameters + params = node.parameters #: Prism::ParametersNode + + params.requireds.each do |param| + if param.is_a?(Prism::RequiredParameterNode) + annotation = param_type_annotations.find { _1.name_location.source.to_sym == param.name } + param_type = annotation&.param_type || Types::Bases::Any.new(location: nil) + required_positionals << Types::Function::Param.new(type: param_type, name: nil) + end + end + + params.optionals.each do |param| + if param.is_a?(Prism::OptionalParameterNode) + annotation = param_type_annotations.find { _1.name_location.source.to_sym == param.name } + param_type = annotation&.param_type || Types::Bases::Any.new(location: nil) + required_keywords[param.name] = Types::Function::Param.new(type: param_type, name: nil) + end + end + + params.keywords.each do |param| + if param.is_a?(Prism::RequiredKeywordParameterNode) + annotation = param_type_annotations.find { _1.name_location.source.to_sym == param.name } + param_type = annotation&.param_type || Types::Bases::Any.new(location: nil) + required_keywords[param.name] = Types::Function::Param.new(type: param_type, name: nil) + elsif param.is_a?(Prism::OptionalKeywordParameterNode) + annotation = param_type_annotations.find { _1.name_location.source.to_sym == param.name } + param_type = annotation&.param_type || Types::Bases::Any.new(location: nil) + required_keywords[param.name] = Types::Function::Param.new(type: param_type, name: nil) + end + end + end + type = Types::Function.new( - required_positionals: [], + required_positionals: required_positionals, optional_positionals: [], rest_positionals: nil, trailing_positionals: [], - required_keywords: {}, + required_keywords: required_keywords, optional_keywords: {}, rest_keywords: nil, return_type: return_type @@ -125,6 +168,15 @@ def self.build(leading_block, trailing_block, variables) next end end + when Annotations::ParamTypeAnnotation + unless type_annotations + type_annotations = DocStyle.new() + end + + if type_annotations.is_a?(DocStyle) + type_annotations.param_type_annotations << paragraph + next + end end unused_annotations << paragraph @@ -144,10 +196,10 @@ def empty? type_annotations.nil? end - def overloads + def overloads(node) case type_annotations when DocStyle - method_type = type_annotations.method_type + method_type = type_annotations.method_type(node) [ AST::Members::MethodDefinition::Overload.new(annotations: [], method_type: method_type) @@ -224,7 +276,7 @@ def location end def overloads - method_type.overloads + method_type.overloads(node) end def overloading? diff --git a/rust/ruby-rbs-sys/build.rs b/rust/ruby-rbs-sys/build.rs index 4e770dd7f..701456cf7 100644 --- a/rust/ruby-rbs-sys/build.rs +++ b/rust/ruby-rbs-sys/build.rs @@ -112,6 +112,7 @@ fn generate_bindings(include_path: &Path) -> Result untyped end + + class ParamTypeAnnotation < Base + attr_reader name_location: Location + + attr_reader colon_location: Location + + attr_reader param_type: Types::t + + attr_reader comment_location: Location? + + def initialize: ( + location: Location, + prefix_location: Location, + name_location: Location, + colon_location: Location, + param_type: Types::t, + comment_location: Location?, + ) -> void + + def map_type_name: () { (TypeName) -> TypeName } -> self + end end end end diff --git a/sig/ast/ruby/members.rbs b/sig/ast/ruby/members.rbs index 66f82e1d0..145c9ea3d 100644 --- a/sig/ast/ruby/members.rbs +++ b/sig/ast/ruby/members.rbs @@ -19,13 +19,17 @@ module RBS class MethodTypeAnnotation class DocStyle + type param_type_annotation = Annotations::ParamTypeAnnotation + attr_accessor return_type_annotation: Annotations::ReturnTypeAnnotation | Annotations::NodeTypeAssertion | nil + attr_reader param_type_annotations: Array[param_type_annotation] + def initialize: () -> void def map_type_name: () { (TypeName) -> TypeName } -> self - def method_type: () -> MethodType + def method_type: (Prism::DefNode) -> MethodType def type_fingerprint: () -> untyped end @@ -53,7 +57,7 @@ module RBS # Returns the method type overloads # - def overloads: () -> Array[AST::Members::MethodDefinition::Overload] + def overloads: (Prism::DefNode) -> Array[AST::Members::MethodDefinition::Overload] def overloading?: () -> bool diff --git a/src/ast.c b/src/ast.c index 6b5eda614..c42021f50 100644 --- a/src/ast.c +++ b/src/ast.c @@ -87,6 +87,8 @@ const char *rbs_node_type_name(rbs_node_t *node) { return "RBS::AST::Ruby::Annotations::ModuleAliasAnnotation"; case RBS_AST_RUBY_ANNOTATIONS_NODE_TYPE_ASSERTION: return "RBS::AST::Ruby::Annotations::NodeTypeAssertion"; + case RBS_AST_RUBY_ANNOTATIONS_PARAM_TYPE_ANNOTATION: + return "RBS::AST::Ruby::Annotations::ParamTypeAnnotation"; case RBS_AST_RUBY_ANNOTATIONS_RETURN_TYPE_ANNOTATION: return "RBS::AST::Ruby::Annotations::ReturnTypeAnnotation"; case RBS_AST_RUBY_ANNOTATIONS_SKIP_ANNOTATION: @@ -983,6 +985,24 @@ rbs_ast_ruby_annotations_node_type_assertion_t *rbs_ast_ruby_annotations_node_ty return instance; } #line 140 "prism/templates/src/ast.c.erb" +rbs_ast_ruby_annotations_param_type_annotation_t *rbs_ast_ruby_annotations_param_type_annotation_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_location_range prefix_location, rbs_location_range name_location, rbs_location_range colon_location, rbs_node_t *param_type, rbs_location_range comment_location) { + rbs_ast_ruby_annotations_param_type_annotation_t *instance = rbs_allocator_alloc(allocator, rbs_ast_ruby_annotations_param_type_annotation_t); + + *instance = (rbs_ast_ruby_annotations_param_type_annotation_t) { + .base = (rbs_node_t) { + .type = RBS_AST_RUBY_ANNOTATIONS_PARAM_TYPE_ANNOTATION, + .location = location, + }, + .prefix_location = prefix_location, + .name_location = name_location, + .colon_location = colon_location, + .param_type = param_type, + .comment_location = comment_location, + }; + + return instance; +} +#line 140 "prism/templates/src/ast.c.erb" rbs_ast_ruby_annotations_return_type_annotation_t *rbs_ast_ruby_annotations_return_type_annotation_new(rbs_allocator_t *allocator, rbs_location_range location, rbs_location_range prefix_location, rbs_location_range return_location, rbs_location_range colon_location, rbs_node_t *return_type, rbs_location_range comment_location) { rbs_ast_ruby_annotations_return_type_annotation_t *instance = rbs_allocator_alloc(allocator, rbs_ast_ruby_annotations_return_type_annotation_t); diff --git a/src/parser.c b/src/parser.c index ffa32dec3..c399ab8a0 100644 --- a/src/parser.c +++ b/src/parser.c @@ -60,6 +60,41 @@ case kRETURN: \ /* nop */ +#define PARAM_NAME_CASES \ + case kBOOL: \ + case kBOT: \ + case kCLASS: \ + case kFALSE: \ + case kINSTANCE: \ + case kINTERFACE: \ + case kNIL: \ + case kSELF: \ + case kSINGLETON: \ + case kTOP: \ + case kTRUE: \ + case kVOID: \ + case kTYPE: \ + case kUNCHECKED: \ + case kIN: \ + case kOUT: \ + case kEND: \ + case kDEF: \ + case kINCLUDE: \ + case kEXTEND: \ + case kPREPEND: \ + case kALIAS: \ + case kMODULE: \ + case kATTRREADER: \ + case kATTRWRITER: \ + case kATTRACCESSOR: \ + case kPUBLIC: \ + case kPRIVATE: \ + case kUNTYPED: \ + case kUSE: \ + case kAS: \ + case k__TODO__: \ + /* nop */ + #define CHECK_PARSE(call) \ if (!call) { \ return false; \ @@ -3632,6 +3667,46 @@ static bool parse_inline_comment(rbs_parser_t *parser, rbs_location_range *comme return true; } +NODISCARD +static bool parse_inline_param_type_annotation(rbs_parser_t *parser, rbs_ast_ruby_annotations_t **annotation, rbs_range_t rbs_range) { + rbs_parser_advance(parser); + + rbs_location_range name_loc = rbs_location_range_current_token(parser); + + ADVANCE_ASSERT(parser, pCOLON); + + rbs_location_range colon_loc = rbs_location_range_current_token(parser); + + rbs_node_t *param_type = NULL; + if (!rbs_parse_type(parser, ¶m_type, false, true, true)) { + return false; + } + + rbs_location_range comment_loc = RBS_LOCATION_NULL_RANGE; + if (!parse_inline_comment(parser, &comment_loc)) { + return false; + } + + rbs_location_range full_loc = { + .start_char = rbs_range.start.char_pos, + .start_byte = rbs_range.start.byte_pos, + .end_char = parser->current_token.range.end.char_pos, + .end_byte = parser->current_token.range.end.byte_pos, + }; + + *annotation = (rbs_ast_ruby_annotations_t *) rbs_ast_ruby_annotations_param_type_annotation_new( + ALLOCATOR(), + full_loc, + RBS_RANGE_LEX2AST(rbs_range), + name_loc, + colon_loc, + param_type, + comment_loc + ); + + return true; +} + NODISCARD static bool parse_inline_leading_annotation(rbs_parser_t *parser, rbs_ast_ruby_annotations_t **annotation) { switch (parser->next_token.type) { @@ -3721,30 +3796,34 @@ static bool parse_inline_leading_annotation(rbs_parser_t *parser, rbs_ast_ruby_a return true; } case kSKIP: { - rbs_parser_advance(parser); + if (parser->next_token2.type == pCOLON) { + return parse_inline_param_type_annotation(parser, annotation, rbs_range); + } else { + rbs_parser_advance(parser); - rbs_range_t skip_range = parser->current_token.range; + rbs_range_t skip_range = parser->current_token.range; - rbs_location_range comment_loc = RBS_LOCATION_NULL_RANGE; - if (!parse_inline_comment(parser, &comment_loc)) { - return false; - } + rbs_location_range comment_loc = RBS_LOCATION_NULL_RANGE; + if (!parse_inline_comment(parser, &comment_loc)) { + return false; + } - rbs_range_t full_range = { - .start = rbs_range.start, - .end = parser->current_token.range.end - }; + rbs_range_t full_range = { + .start = rbs_range.start, + .end = parser->current_token.range.end + }; - rbs_location_range full_loc = RBS_RANGE_LEX2AST(full_range); + rbs_location_range full_loc = RBS_RANGE_LEX2AST(full_range); - *annotation = (rbs_ast_ruby_annotations_t *) rbs_ast_ruby_annotations_skip_annotation_new( - ALLOCATOR(), - full_loc, - RBS_RANGE_LEX2AST(rbs_range), - RBS_RANGE_LEX2AST(skip_range), - comment_loc - ); - return true; + *annotation = (rbs_ast_ruby_annotations_t *) rbs_ast_ruby_annotations_skip_annotation_new( + ALLOCATOR(), + full_loc, + RBS_RANGE_LEX2AST(rbs_range), + RBS_RANGE_LEX2AST(skip_range), + comment_loc + ); + return true; + } } case kRETURN: { rbs_parser_advance(parser); @@ -3828,6 +3907,10 @@ static bool parse_inline_leading_annotation(rbs_parser_t *parser, rbs_ast_ruby_a ); return true; } + case tLIDENT: + PARAM_NAME_CASES { + return parse_inline_param_type_annotation(parser, annotation, rbs_range); + } default: { rbs_parser_set_error(parser, parser->next_token, true, "unexpected token for @rbs annotation"); return false; diff --git a/templates/include/rbs/ast.h.erb b/templates/include/rbs/ast.h.erb index a55b4f047..9497d9c63 100644 --- a/templates/include/rbs/ast.h.erb +++ b/templates/include/rbs/ast.h.erb @@ -110,6 +110,7 @@ typedef union rbs_ast_ruby_annotations { rbs_ast_ruby_annotations_node_type_assertion_t node_type_assertion; rbs_ast_ruby_annotations_return_type_annotation_t return_type_annotation; rbs_ast_ruby_annotations_skip_annotation_t skip_annotation; + rbs_ast_ruby_annotations_param_type_annotation_t param_type_annotation; } rbs_ast_ruby_annotations_t; /// `rbs_ast_symbol_t` models user-defined identifiers like class names, method names, etc. diff --git a/test/rbs/inline_annotation_parsing_test.rb b/test/rbs/inline_annotation_parsing_test.rb index 020e8df78..1cce40e54 100644 --- a/test/rbs/inline_annotation_parsing_test.rb +++ b/test/rbs/inline_annotation_parsing_test.rb @@ -226,10 +226,6 @@ def test_error__instance_variable Parser.parse_inline_leading_annotation("@rbs @name:", 0...) end - assert_raises RBS::ParsingError do - Parser.parse_inline_leading_annotation("@rbs name: String", 0...) - end - assert_raises RBS::ParsingError do Parser.parse_inline_leading_annotation("@rbs @name: void", 0...) end @@ -362,4 +358,35 @@ def test_parse__method_types_annotation__only_dot3 assert_equal "...", annot.dot3_location.source end end + + def test_parse__param_type + Parser.parse_inline_leading_annotation("@rbs x: untyped", 0...).tap do |annot| + assert_instance_of AST::Ruby::Annotations::ParamTypeAnnotation, annot + assert_equal "@rbs x: untyped", annot.location.source + assert_equal "x", annot.name_location.source + assert_equal ":", annot.colon_location.source + assert_equal "untyped", annot.param_type.location.source + assert_nil annot.comment_location + end + + Parser.parse_inline_leading_annotation("@rbs abc: untyped -- some comment here", 0...).tap do |annot| + assert_instance_of AST::Ruby::Annotations::ParamTypeAnnotation, annot + assert_equal "@rbs abc: untyped -- some comment here", annot.location.source + assert_equal "abc", annot.name_location.source + assert_equal ":", annot.colon_location.source + assert_equal "untyped", annot.param_type.location.source + assert_equal "-- some comment here", annot.comment_location.source + end + end + + def test_parse__param_type__skip + Parser.parse_inline_leading_annotation("@rbs skip: untyped", 0...).tap do |annot| + assert_instance_of AST::Ruby::Annotations::ParamTypeAnnotation, annot + assert_equal "@rbs skip: untyped", annot.location.source + assert_equal "skip", annot.name_location.source + assert_equal ":", annot.colon_location.source + assert_equal "untyped", annot.param_type.location.source + assert_nil annot.comment_location + end + end end diff --git a/test/rbs/inline_parser_test.rb b/test/rbs/inline_parser_test.rb index 6f834817a..01449f1e0 100644 --- a/test/rbs/inline_parser_test.rb +++ b/test/rbs/inline_parser_test.rb @@ -275,6 +275,41 @@ def add(x, y) end end + def test_parse__def_method_docs + result = parse(<<~RUBY) + class Foo + # @rbs x: Integer + # @rbs y: Integer + # @rbs a: String + # @rbs b: String? + # @rbs return: String + def add(x, y = 3, a:, b: nil) + end + end + RUBY + + assert_empty result.diagnostics + + result.declarations[0].tap do |decl| + decl.members[0].tap do |member| + assert_instance_of RBS::AST::Ruby::Members::DefMember, member + assert_equal ["(Integer, y: Integer, a: String, b: String?) -> String"], member.overloads.map { _1.method_type.to_s } + end + end + end + + + def test_error__def_method_docs + result = parse(<<~RUBY) + class Foo + # @rbs x: Integer + # @rbs return: void + def add(y) + end + end + RUBY + end + def test_parse__skip_class_module result = parse(<<~RUBY) # @rbs skip -- not a constant @@ -767,25 +802,22 @@ class Foo end RUBY - # The @rbs annotations should be reported as syntax errors (invalid format) + # The @rbs annotations should be reported as unused (ParamTypeAnnotation is valid but not applicable to attr_*) assert_equal 3, result.diagnostics.size assert_any!(result.diagnostics) do |diagnostic| - assert_instance_of RBS::InlineParser::Diagnostic::AnnotationSyntaxError, diagnostic + assert_instance_of RBS::InlineParser::Diagnostic::UnusedInlineAnnotation, diagnostic assert_equal "@rbs name: String", diagnostic.location.source - assert_match(/Syntax error:/, diagnostic.message) end assert_any!(result.diagnostics) do |diagnostic| - assert_instance_of RBS::InlineParser::Diagnostic::AnnotationSyntaxError, diagnostic + assert_instance_of RBS::InlineParser::Diagnostic::UnusedInlineAnnotation, diagnostic assert_equal "@rbs age: Integer", diagnostic.location.source - assert_match(/Syntax error:/, diagnostic.message) end assert_any!(result.diagnostics) do |diagnostic| - assert_instance_of RBS::InlineParser::Diagnostic::AnnotationSyntaxError, diagnostic + assert_instance_of RBS::InlineParser::Diagnostic::UnusedInlineAnnotation, diagnostic assert_equal "@rbs data: Array[Hash[Symbol, untyped]]", diagnostic.location.source - assert_match(/Syntax error:/, diagnostic.message) end result.declarations[0].tap do |decl|