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 b19937aa1..ecd1b65f2 100644 --- a/ext/rbs_extension/ast_translation.c +++ b/ext/rbs_extension/ast_translation.c @@ -923,6 +923,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..4b44f394d 100644 --- a/lib/rbs/ast/ruby/members.rb +++ b/lib/rbs/ast/ruby/members.rb @@ -17,19 +17,211 @@ def initialize(buffer) class MethodTypeAnnotation class DocStyle attr_accessor :return_type_annotation + attr_reader :required_positionals + attr_reader :optional_positionals + attr_accessor :rest_positionals + attr_reader :trailing_positionals + attr_reader :required_keywords + attr_reader :optional_keywords + attr_accessor :rest_keywords def initialize @return_type_annotation = nil + @required_positionals = [] #: Array[Annotations::ParamTypeAnnotation?] + @optional_positionals = [] #: Array[Annotations::ParamTypeAnnotation?] + @rest_positionals = nil #: Annotations::ParamTypeAnnotation? + @trailing_positionals = [] #: Array[Annotations::ParamTypeAnnotation?] + @required_keywords = {} #: Hash[Symbol, Annotations::ParamTypeAnnotation] + @optional_keywords = {} #: Hash[Symbol, Annotations::ParamTypeAnnotation] + @rest_keywords = nil #: Annotations::ParamTypeAnnotation? + end + + def self.build(param_type_annotations, return_type_annotation, node) + doc = DocStyle.new + doc.return_type_annotation = return_type_annotation + + unused = param_type_annotations.dup + + if node.parameters + params = node.parameters #: Prism::ParametersNode + + params.requireds.each do |param| + if param.is_a?(Prism::RequiredParameterNode) + annotation = unused.find { _1.name_location.source.to_sym == param.name } + if annotation + unused.delete(annotation) + doc.required_positionals << annotation + else + doc.required_positionals << param.name + end + end + end + + params.optionals.each do |param| + if param.is_a?(Prism::OptionalParameterNode) + annotation = unused.find { _1.name_location.source.to_sym == param.name } + if annotation + unused.delete(annotation) + doc.optional_positionals << annotation + else + doc.optional_positionals << param.name + end + end + end + + if (rest = params.rest) && rest.is_a?(Prism::RestParameterNode) && rest.name + annotation = unused.find { _1.name_location.source.to_sym == rest.name } + if annotation + unused.delete(annotation) + doc.rest_positionals = annotation + else + doc.rest_positionals = rest.name + end + end + + params.posts.each do |param| + if param.is_a?(Prism::RequiredParameterNode) + annotation = unused.find { _1.name_location.source.to_sym == param.name } + if annotation + unused.delete(annotation) + doc.trailing_positionals << annotation + else + doc.trailing_positionals << param.name + end + end + end + + params.keywords.each do |param| + case param + when Prism::RequiredKeywordParameterNode + annotation = unused.find { _1.name_location.source.to_sym == param.name } + if annotation + unused.delete(annotation) + doc.required_keywords[param.name] = annotation + else + doc.required_keywords[param.name] = param.name + end + when Prism::OptionalKeywordParameterNode + annotation = unused.find { _1.name_location.source.to_sym == param.name } + if annotation + unused.delete(annotation) + doc.optional_keywords[param.name] = annotation + else + doc.optional_keywords[param.name] = param.name + end + end + end + + if (kw_rest = params.keyword_rest) && kw_rest.is_a?(Prism::KeywordRestParameterNode) && kw_rest.name + annotation = unused.find { _1.name_location.source.to_sym == kw_rest.name } + if annotation + unused.delete(annotation) + doc.rest_keywords = annotation + else + doc.rest_keywords = kw_rest.name + end + end + end + + [doc, unused] + end + + def all_param_annotations + annotations = [] #: Array[param_type_annotation | Symbol | nil] + # + required_positionals.each { |a| annotations << a } + optional_positionals.each { |a| annotations << a } + annotations << rest_positionals + trailing_positionals.each { |a| annotations << a } + required_keywords.each_value { |a| annotations << a } + optional_keywords.each_value { |a| annotations << a } + annotations << rest_keywords + + annotations end def map_type_name(&block) DocStyle.new.tap do |new| new.return_type_annotation = return_type_annotation&.map_type_name(&block) + new.required_positionals.replace( + required_positionals.map do |a| + case a + when Annotations::ParamTypeAnnotation + a.map_type_name(&block) + else + a + end + end + ) + new.optional_positionals.replace( + optional_positionals.map do |a| + case a + when Annotations::ParamTypeAnnotation + a.map_type_name(&block) + else + a + end + end + ) + new.rest_positionals = + case rest_positionals + when Annotations::ParamTypeAnnotation + rest_positionals.map_type_name(&block) + else + rest_positionals + end + new.trailing_positionals.replace( + trailing_positionals.map do |a| + case a + when Annotations::ParamTypeAnnotation + a.map_type_name(&block) + else + a + end + end + ) + new.required_keywords.replace( + required_keywords.transform_values do |a| + case a + when Annotations::ParamTypeAnnotation + a.map_type_name(&block) + else + a + end + end + ) + new.optional_keywords.replace( + optional_keywords.transform_values do |a| + case a + when Annotations::ParamTypeAnnotation + a.map_type_name(&block) + else + a + end + end + ) + new.rest_keywords = + case rest_keywords + when Annotations::ParamTypeAnnotation + rest_keywords.map_type_name(&block) + else + rest_keywords + end end #: self end def type_fingerprint - return_type_annotation&.type_fingerprint + [ + return_type_annotation&.type_fingerprint, + all_param_annotations.map do |param| + case param + when Annotations::ParamTypeAnnotation + param.type_fingerprint + else + param + end + end + ] end def method_type @@ -43,14 +235,77 @@ def method_type Types::Bases::Any.new(location: nil) end + any = -> { Types::Bases::Any.new(location: nil) } + + req_pos = required_positionals.map do |a| + case a + when Annotations::ParamTypeAnnotation + Types::Function::Param.new(type: a.param_type, name: a.name_location.source.to_sym) + else + Types::Function::Param.new(type: any.call, name: a) + end + end + opt_pos = optional_positionals.map do |a| + case a + when Annotations::ParamTypeAnnotation + Types::Function::Param.new(type: a.param_type, name: a.name_location.source.to_sym) + else + Types::Function::Param.new(type: any.call, name: a) + end + end + rest_pos = + case rest_positionals + when Annotations::ParamTypeAnnotation + Types::Function::Param.new(type: rest_positionals.param_type, name: rest_positionals.name_location.source.to_sym) + when Symbol + Types::Function::Param.new(type: any.call, name: rest_positionals) + else + nil + end + trail_pos = trailing_positionals.map do |a| + case a + when Annotations::ParamTypeAnnotation + Types::Function::Param.new(type: a.param_type, name: a.name_location.source.to_sym) + else + Types::Function::Param.new(type: any.call, name: a) + end + end + + req_kw = required_keywords.transform_values do |a| + case a + when Annotations::ParamTypeAnnotation + Types::Function::Param.new(type: a.param_type, name: nil) + else + Types::Function::Param.new(type: any.call, name: nil) + end + end + opt_kw = optional_keywords.transform_values do |a| + case a + when Annotations::ParamTypeAnnotation + Types::Function::Param.new(type: a.param_type, name: nil) + else + Types::Function::Param.new(type: any.call, name: nil) + end + end + + rest_kw = + case rest_keywords + when Annotations::ParamTypeAnnotation + Types::Function::Param.new(type: rest_keywords.param_type, name: nil) + when Symbol + Types::Function::Param.new(type: any.call, name: nil) + else + nil + end + type = Types::Function.new( - required_positionals: [], - optional_positionals: [], - rest_positionals: nil, - trailing_positionals: [], - required_keywords: {}, - optional_keywords: {}, - rest_keywords: nil, + required_positionals: req_pos, + optional_positionals: opt_pos, + rest_positionals: rest_pos, + trailing_positionals: trail_pos, + required_keywords: req_kw, + optional_keywords: opt_kw, + rest_keywords: rest_kw, return_type: return_type ) @@ -82,17 +337,18 @@ def map_type_name(&block) MethodTypeAnnotation.new(type_annotations: updated_annots) #: self end - def self.build(leading_block, trailing_block, variables) + def self.build(leading_block, trailing_block, variables, node) unused_annotations = [] #: Array[Annotations::leading_annotation | CommentBlock::AnnotationSyntaxError] unused_trailing_annotation = nil #: Annotations::trailing_annotation | CommentBlock::AnnotationSyntaxError | nil type_annotations = nil #: type_annotations + return_annotation = nil #: Annotations::ReturnTypeAnnotation | Annotations::NodeTypeAssertion | nil + param_annotations = [] #: Array[Annotations::ParamTypeAnnotation] if trailing_block case annotation = trailing_block.trailing_annotation(variables) when Annotations::NodeTypeAssertion - type_annotations = DocStyle.new() - type_annotations.return_type_annotation = annotation + return_annotation = annotation else unused_trailing_annotation = annotation end @@ -116,25 +372,30 @@ def self.build(leading_block, trailing_block, variables) end when Annotations::ReturnTypeAnnotation unless type_annotations - type_annotations = DocStyle.new() - end - - if type_annotations.is_a?(DocStyle) - unless type_annotations.return_type_annotation - type_annotations.return_type_annotation = paragraph + unless return_annotation + return_annotation = paragraph next end end + when Annotations::ParamTypeAnnotation + unless type_annotations + param_annotations << paragraph + next + end end unused_annotations << paragraph end end + if !type_annotations && (return_annotation || !param_annotations.empty?) + doc_style, unused_params = DocStyle.build(param_annotations, return_annotation, node) + type_annotations = doc_style + unused_annotations.concat(unused_params) + end + [ - MethodTypeAnnotation.new( - type_annotations: type_annotations - ), + MethodTypeAnnotation.new(type_annotations: type_annotations), unused_annotations, unused_trailing_annotation ] diff --git a/lib/rbs/inline_parser.rb b/lib/rbs/inline_parser.rb index 3a18a7534..584a4f087 100644 --- a/lib/rbs/inline_parser.rb +++ b/lib/rbs/inline_parser.rb @@ -196,7 +196,7 @@ def visit_def_node(node) trailing_block = comments.trailing_block!(end_loc) end - method_type, leading_unuseds, trailing_unused = AST::Ruby::Members::MethodTypeAnnotation.build(leading_block, trailing_block, []) + method_type, leading_unuseds, trailing_unused = AST::Ruby::Members::MethodTypeAnnotation.build(leading_block, trailing_block, [], node) report_unused_annotation(trailing_unused, *leading_unuseds) defn = AST::Ruby::Members::DefMember.new(buffer, node.name, node, method_type, leading_block) 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..12c775ffb 100644 --- a/sig/ast/ruby/members.rbs +++ b/sig/ast/ruby/members.rbs @@ -19,10 +19,24 @@ module RBS class MethodTypeAnnotation class DocStyle + type param_type_annotation = Annotations::ParamTypeAnnotation + attr_accessor return_type_annotation: Annotations::ReturnTypeAnnotation | Annotations::NodeTypeAssertion | nil + attr_reader required_positionals: Array[param_type_annotation | Symbol] + attr_reader optional_positionals: Array[param_type_annotation | Symbol] + attr_accessor rest_positionals: param_type_annotation | Symbol | nil + attr_reader trailing_positionals: Array[param_type_annotation | Symbol] + attr_reader required_keywords: Hash[Symbol, param_type_annotation | Symbol] + attr_reader optional_keywords: Hash[Symbol, param_type_annotation | Symbol] + attr_accessor rest_keywords: param_type_annotation | Symbol | nil + def initialize: () -> void + def self.build: (Array[param_type_annotation], Annotations::ReturnTypeAnnotation | Annotations::NodeTypeAssertion | nil, Prism::DefNode) -> [DocStyle, Array[param_type_annotation]] + + def all_param_annotations: () -> Array[param_type_annotation | Symbol | nil] + def map_type_name: () { (TypeName) -> TypeName } -> self def method_type: () -> MethodType @@ -42,7 +56,7 @@ module RBS # # Returns a tuple of `DefAnnotations` object, array of unused leading annotations, and unused trailing annotation. # - def self.build: (CommentBlock? leading_block, CommentBlock? trailing_block, Array[Symbol]) -> [ + def self.build: (CommentBlock? leading_block, CommentBlock? trailing_block, Array[Symbol], Prism::DefNode) -> [ MethodTypeAnnotation, Array[Annotations::leading_annotation | CommentBlock::AnnotationSyntaxError], Annotations::trailing_annotation | CommentBlock::AnnotationSyntaxError | nil 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 be238c759..1e9c1fa61 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; \ @@ -3635,6 +3670,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) { @@ -3724,30 +3799,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); @@ -3831,6 +3910,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..d9b4f72ef 100644 --- a/test/rbs/inline_parser_test.rb +++ b/test/rbs/inline_parser_test.rb @@ -275,6 +275,55 @@ 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 x, ?Integer y, 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 + + assert_equal 1, result.diagnostics.size + + assert_any!(result.diagnostics) do |diagnostic| + assert_instance_of RBS::InlineParser::Diagnostic::UnusedInlineAnnotation, diagnostic + assert_equal "@rbs x: Integer", diagnostic.location.source + end + + result.declarations[0].tap do |decl| + decl.members[0].tap do |member| + assert_instance_of RBS::AST::Ruby::Members::DefMember, member + assert_equal ["(untyped y) -> void"], member.overloads.map { _1.method_type.to_s } + end + end + end + def test_parse__skip_class_module result = parse(<<~RUBY) # @rbs skip -- not a constant @@ -767,25 +816,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|