yard-inline-rbs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YardInlineRbs
4
+ # The parameter names of a method object, as YARD records them: 'x', '*rest', 'name:'.
5
+ class ParameterNames
6
+ # @rbs @object: YARD::CodeObjects::Base
7
+ # @rbs @raw: Array[String]?
8
+
9
+ # The key of a keyword rest without a name, in the keywords of `RbsFunctionTypes`.
10
+ KEYWORD_REST_KEY = '**' #: String
11
+
12
+ # The name of an unnamed parameter, e.g. of a method that only exists in a `.rbs` file.
13
+ # @rbs (Integer index) -> String
14
+ def self.placeholder(index)
15
+ "arg#{index}"
16
+ end
17
+
18
+ # @rbs (YARD::CodeObjects::Base object) -> void
19
+ def initialize(object)
20
+ @object = object
21
+ end
22
+
23
+ # @rbs () -> bool
24
+ def empty?
25
+ raw.empty?
26
+ end
27
+
28
+ # The positional names in Ruby order, without the '*' prefix.
29
+ # @rbs () -> Array[String]
30
+ def positional
31
+ raw.reject {|name| name.end_with?(':') || name.start_with?('**', '&')}
32
+ .map {|name| name.delete_prefix('*')}
33
+ end
34
+
35
+ # All names as @param tags spell them; YARD renders them in this order.
36
+ # @rbs () -> Array[String]
37
+ def tag_order
38
+ raw.map {|name| name.sub(/\A[*&]+/, '').delete_suffix(':')}
39
+ end
40
+
41
+ # Resolves the placeholder of an unnamed splat ('*') or keyword rest ('**') annotation.
42
+ #
43
+ # Returns `name` itself if it is no placeholder.
44
+ # @rbs (String name) -> String
45
+ def resolve_splat(name)
46
+ return name unless %w[* **].include?(name)
47
+
48
+ raw_name = raw.find do |param_name|
49
+ param_name.start_with?(name) && !param_name.start_with?("#{name}*")
50
+ end
51
+ actual_name = raw_name.to_s.delete_prefix(name)
52
+ actual_name.empty? ? name : actual_name
53
+ end
54
+
55
+ private
56
+
57
+ # @rbs () -> Array[String]
58
+ def raw
59
+ object = @object
60
+ return [] unless object.is_a?(YARD::CodeObjects::MethodObject)
61
+
62
+ @raw ||= object.parameters.map(&:first)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YardInlineRbs
4
+ # What an annotation types, as the warnings name it and InlineTypes records it.
5
+ module Annotated
6
+ RETURN_TYPE = 'the return type' #: String
7
+ BLOCK = 'the block' #: String
8
+ SIGNATURE = 'the signature' #: String
9
+
10
+ # @rbs (String name) -> String
11
+ def self.parameter(name)
12
+ "parameter '#{name}'"
13
+ end
14
+ end
15
+
16
+ # The annotations of one method, attribute or constant, collected while parsing. The first
17
+ # annotation of a parameter, the return value or the block wins; a later one is recorded as a
18
+ # duplicate. An annotation whose type could not be parsed does not count.
19
+ class ParseResult
20
+ # @rbs @signature_added: bool
21
+
22
+ # The positional parameters, in Ruby order.
23
+ attr_reader :positional_param_types #: Array[PositionalParamType]
24
+ attr_reader :named_param_types #: Hash[String, NamedParamAnnotation]
25
+ attr_reader :return_type #: String?
26
+ attr_reader :return_description #: String?
27
+ attr_reader :block_annotation #: BlockAnnotation?
28
+ # The signatures of a method with overloads, which are no parameters of the method itself.
29
+ attr_reader :overloads #: Array[signature]
30
+ # The annotations that are not valid RBS.
31
+ attr_reader :parse_errors #: Array[String]
32
+ # The valid annotations the plugin does not use.
33
+ attr_reader :unsupported_annotations #: Array[String]
34
+ # What was annotated more than once, such as `parameter 'count'` or `the return type`.
35
+ attr_reader :duplicate_annotations #: Array[String]
36
+
37
+ # @rbs positional_param_types: Array[PositionalParamType]
38
+ # @rbs named_param_types: Hash[String, NamedParamAnnotation]
39
+ # @rbs return_type: String?
40
+ # @rbs return_description: String?
41
+ # @rbs block_annotation: BlockAnnotation?
42
+ # @rbs return: void
43
+ def initialize(positional_param_types: [], named_param_types: {}, return_type: nil,
44
+ return_description: nil, block_annotation: nil)
45
+ @positional_param_types = positional_param_types
46
+ @named_param_types = named_param_types
47
+ @return_type = return_type
48
+ @return_description = return_description
49
+ @block_annotation = block_annotation
50
+ @overloads = [] #: Array[signature]
51
+ @parse_errors = []
52
+ @unsupported_annotations = []
53
+ @duplicate_annotations = []
54
+ @signature_added = false
55
+ end
56
+
57
+ # @return [Boolean] Whether there is no type to apply.
58
+ # @rbs () -> bool
59
+ def empty?
60
+ @positional_param_types.empty? && @named_param_types.empty? && @return_type.nil? &&
61
+ @block_annotation.nil? && @overloads.empty?
62
+ end
63
+
64
+ # @rbs (String? type, ?String? description) -> void
65
+ def add_return_type(type, description = nil)
66
+ return if type.nil?
67
+ return add_duplicate(Annotated::RETURN_TYPE) if @return_type
68
+
69
+ @return_type = type
70
+ @return_description = description
71
+ end
72
+
73
+ # @rbs (String name, NamedParamAnnotation annotation) -> void
74
+ def add_named_param(name, annotation)
75
+ return if annotation.type.nil?
76
+ return add_duplicate(Annotated.parameter(name)) if @named_param_types.key?(name)
77
+
78
+ @named_param_types[name] = annotation
79
+ end
80
+
81
+ # @rbs (BlockAnnotation? block_annotation) -> void
82
+ def add_block(block_annotation)
83
+ return if block_annotation.nil?
84
+ return add_duplicate(Annotated::BLOCK) if @block_annotation
85
+
86
+ @block_annotation = block_annotation
87
+ end
88
+
89
+ # Adds the signature of a method, or its overloads when it has several. Nothing for none.
90
+ # @rbs (Array[signature] signatures) -> void
91
+ def add_signatures(signatures)
92
+ return add_signature(signatures.first) if signatures.size < 2
93
+
94
+ add_overloads(signatures)
95
+ end
96
+
97
+ # Adds the signatures of a method with overloads, as TypeParser#method_signature returns them.
98
+ # Like a second signature, a second set of overloads is one duplicate.
99
+ # @rbs (Array[signature] signatures) -> void
100
+ def add_overloads(signatures)
101
+ return add_duplicate(Annotated::SIGNATURE) if @signature_added
102
+
103
+ @signature_added = true
104
+ @overloads.concat(signatures)
105
+ end
106
+
107
+ # Adds the parts of a method signature, as TypeParser#method_signature returns them, or
108
+ # nothing for nil, an invalid signature. A second signature is one duplicate, not one for each
109
+ # of its parts.
110
+ # @rbs (signature? signature) -> void
111
+ def add_signature(signature)
112
+ return if signature.nil?
113
+ return add_duplicate(Annotated::SIGNATURE) if @signature_added
114
+
115
+ @signature_added = true
116
+ @positional_param_types.concat(signature[:positional_types])
117
+ signature[:named_types].each {|name, annotation| add_named_param(name, annotation)}
118
+ add_return_type(signature[:return_type])
119
+ add_block(signature[:block_annotation])
120
+ end
121
+
122
+ # Records that `annotated`, an Annotated value, was typed again.
123
+ # @rbs (String annotated) -> void
124
+ def add_duplicate(annotated)
125
+ @duplicate_annotations << annotated
126
+ end
127
+ end
128
+
129
+ # The type and description of a positional parameter.
130
+ class PositionalParamType
131
+ attr_reader :type #: String?
132
+ attr_reader :name #: String?
133
+ attr_reader :description #: String?
134
+
135
+ # @rbs type: String?
136
+ # @rbs name: String?
137
+ # @rbs description: String?
138
+ # @rbs return: void
139
+ def initialize(type:, name: nil, description: nil)
140
+ @type = type
141
+ @name = name
142
+ @description = description
143
+ end
144
+ end
145
+
146
+ # The type and description of a keyword parameter, or of a splat. `rest` is true for a keyword
147
+ # rest (`**options`), which collects the other keywords.
148
+ class NamedParamAnnotation
149
+ attr_reader :type #: String?
150
+ attr_reader :description #: String?
151
+ attr_reader :rest #: bool
152
+
153
+ # @rbs type: String?
154
+ # @rbs description: String?
155
+ # @rbs rest: bool
156
+ # @rbs return: void
157
+ def initialize(type:, description: nil, rest: false)
158
+ @type = type
159
+ @description = description
160
+ @rest = rest
161
+ end
162
+ end
163
+
164
+ # The types a block yields and returns.
165
+ class BlockAnnotation
166
+ attr_reader :yield_types #: Array[String?]
167
+ attr_reader :return_type #: String?
168
+
169
+ # @rbs yield_types: Array[String?]
170
+ # @rbs return_type: String?
171
+ # @rbs return: void
172
+ def initialize(yield_types:, return_type:)
173
+ @yield_types = yield_types
174
+ @return_type = return_type
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ YARD::Tags::Library.define_tag('RBS annotation', :rbs)
4
+
5
+ YARD::DocstringParser.after_parse do |parser|
6
+ YardInlineRbs::TagInjector.new(parser).inject
7
+ end
8
+
9
+ YardInlineRbs::RbsHandlerPatches.install
10
+
11
+ # Also runs for YARD.parse_string. A callback that returns false would skip the file.
12
+ YARD::Parser::SourceParser.before_parse_file do |_parser|
13
+ YardInlineRbs::AnnotationWarnings.clear_logged
14
+ end
15
+
16
+ YARD::Parser::SourceParser.before_parse_list do |files, _globals|
17
+ YardInlineRbs::DeclarationSource.clear_cache
18
+ YardInlineRbs::RbsFileTags.parse_rbs_first(files)
19
+ end
20
+
21
+ YARD::Parser::SourceParser.after_parse_list do |files, _globals|
22
+ YardInlineRbs::RbsFileTags.apply(files)
23
+ YardInlineRbs::DeclarationSource.clear_cache # Frees the lines of all files.
24
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YardInlineRbs
4
+ # Collects the RBS annotations of `@rbs` tags, `#:` description lines and a trailing `#:`.
5
+ class RbsCommentParser
6
+ # @rbs @rbs_tag_texts: Array[String]
7
+ # @rbs @inline_annotations: Array[String]
8
+ # @rbs @source_line: String?
9
+ # @rbs @result: ParseResult
10
+ # @rbs @types: TypeParser
11
+
12
+ # @rbs rbs_tag_texts: Array[String] -- The texts of the `@rbs` tags, which YARD strips.
13
+ # @rbs inline_annotations: Array[String]
14
+ # @rbs source_line: String? -- The declaration line, for a trailing `#:`; nil to skip it.
15
+ # @rbs return: void
16
+ def initialize(rbs_tag_texts, inline_annotations, source_line)
17
+ @rbs_tag_texts = rbs_tag_texts
18
+ @inline_annotations = inline_annotations
19
+ @source_line = source_line
20
+ end
21
+
22
+ # @rbs () -> ParseResult
23
+ def parse
24
+ @result = ParseResult.new
25
+ @types = TypeParser.new(@result.parse_errors)
26
+ inline = InlineAnnotations.new(@types, @result)
27
+
28
+ @rbs_tag_texts.each {|text| process_rbs_tag(text)}
29
+ @inline_annotations.each {|line| inline.process_line(line)}
30
+ source_line = @source_line
31
+ inline.process_trailing(source_line) if source_line
32
+
33
+ @result
34
+ end
35
+
36
+ private
37
+
38
+ # A description after `--` would make a plain type, such as `[Integer, String] -- The pair.`,
39
+ # look like a method type, so only the type decides.
40
+ # @rbs (String text) -> void
41
+ def process_rbs_tag(text)
42
+ return if type_checker_only_annotation?(text)
43
+ return process_ivar_annotation(text) if text.start_with?('@')
44
+ return process_block_annotation(text) if text.start_with?('&')
45
+ return process_splat_annotation(text) if text.match?(/\A\*{1,2}\w*:/)
46
+ return record_unsupported(text) if structural_rbs_annotation?(text)
47
+
48
+ type_str, = RbsDescription.split(text)
49
+ return process_method_signature(text) if @types.method_type?(type_str)
50
+
51
+ process_named_or_standalone(text)
52
+ end
53
+
54
+ # @rbs (String text) -> void
55
+ def process_named_or_standalone(text)
56
+ if (match = text.match(/\A(\w+):(?!:)\s*(.+)/m))
57
+ process_named_annotation(match[1], match[2])
58
+ else
59
+ process_standalone_return_type(text)
60
+ end
61
+ end
62
+
63
+ # @rbs (String text) -> void
64
+ def record_unsupported(text)
65
+ @result.unsupported_annotations << text
66
+ end
67
+
68
+ # Annotations for the type checker only: `skip`, `...` and `module-self:`, and `override`,
69
+ # `generic`, `inherits` and `use` of the rbs-inline gem.
70
+ # @rbs (String text) -> bool
71
+ def type_checker_only_annotation?(text)
72
+ %w[skip ... override].include?(text) ||
73
+ text.start_with?('module-self:', 'generic ', 'inherits ', 'use ')
74
+ end
75
+
76
+ # An unnamed splat (`*: Type`) is recorded as '*' or '**'; TagInjector resolves the name.
77
+ # @rbs (String text) -> void
78
+ def process_splat_annotation(text)
79
+ match = text.match(/\A(\*{1,2})(\w*):\s*(.+)/m)
80
+ return @result.parse_errors << text unless match
81
+
82
+ stars = match[1].to_s
83
+ name = match[2].to_s
84
+ type_str, description = RbsDescription.split(match[3])
85
+ yard_type = @types.type(type_str)
86
+ @result.add_named_param(name.empty? ? stars : name,
87
+ NamedParamAnnotation.new(type: yard_type, description: description))
88
+ end
89
+
90
+ # Declarations (def, class, end, attr_*) describe structure, not types.
91
+ # @rbs (String text) -> bool
92
+ def structural_rbs_annotation?(text)
93
+ text == 'end' ||
94
+ text.start_with?('def ', 'class ', 'attr_reader ', 'attr_writer ', 'attr_accessor ')
95
+ end
96
+
97
+ # The `?` of an optional block (`&block: ? () -> void`) is ignored.
98
+ # @rbs (String text) -> void
99
+ def process_block_annotation(text)
100
+ match = text.match(/\A&\w*:\s*(.+)/m)
101
+ return @result.parse_errors << text unless match
102
+
103
+ type_str, = RbsDescription.split(match[1])
104
+ @result.add_block(@types.block_annotation(type_str.delete_prefix('?')))
105
+ end
106
+
107
+ # @rbs (String text) -> void
108
+ def process_standalone_return_type(text)
109
+ type_str, = RbsDescription.split(text)
110
+ @result.add_return_type(@types.type(type_str))
111
+ end
112
+
113
+ # @rbs (String text) -> void
114
+ def process_ivar_annotation(text)
115
+ match = text.match(/\A@@?(\w+):\s*(.+)/m)
116
+ return unless match
117
+
118
+ type_str, = RbsDescription.split(match[2])
119
+ @types.type(type_str) # An invalid type is reported, although the annotation is not used.
120
+ @result.unsupported_annotations << text
121
+ end
122
+
123
+ # @rbs (String key, String value) -> void
124
+ def process_named_annotation(key, value)
125
+ type_str, description = RbsDescription.split(value)
126
+ yard_type = @types.type(type_str)
127
+
128
+ if key == 'return'
129
+ @result.add_return_type(yard_type, description)
130
+ else
131
+ @result.add_named_param(key, NamedParamAnnotation.new(type: yard_type,
132
+ description: description))
133
+ end
134
+ end
135
+
136
+ # A signature with overloads that cannot all be parsed is documented with the ones that can.
137
+ # @rbs (String text) -> void
138
+ def process_method_signature(text)
139
+ overloads = RbsDescription.overloads(text)
140
+ @result.add_signatures(overloads.filter_map {|overload| @types.method_signature(overload)})
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YardInlineRbs
4
+ # Splits an annotation value into its type and description: `Integer -- the count`.
5
+ # The type and the description may continue on the lines below, and the ` -- ` may also end a
6
+ # line or start one. A line starting with `| (` starts another overload of a method type.
7
+ class RbsDescription
8
+ # Before a description: ` -- `, or ` --` at the end of a line.
9
+ SEPARATOR = /\s--(?:\s|\z)/ #: Regexp
10
+
11
+ # A line that starts another overload, such as `| (Float) -> Float`. A union type can
12
+ # continue on a line starting with `| Float`, or `| [Float, String]`, so a line only starts an
13
+ # overload after a line with the arrow of a method type.
14
+ OVERLOAD = /\A\|\s*(?:[(\[{]|->)/ #: Regexp
15
+
16
+ # The type of every overload of a method type, without the bars and the descriptions. A type
17
+ # that is no overload, such as a union, is one.
18
+ # @rbs (String? text) -> Array[String]
19
+ def self.overloads(text)
20
+ groups(text).each_with_index.map do |group, index|
21
+ overload = group.join(' ')
22
+ overload = overload.delete_prefix('|') if index.positive?
23
+ overload.split(SEPARATOR).first.to_s.strip
24
+ end
25
+ end
26
+
27
+ # The type of the first overload, and the description, if any.
28
+ # @rbs (String? text) -> [String, String?]
29
+ def self.split(text)
30
+ type_string, description = groups(text).first.to_a.join(' ').split(SEPARATOR, 2)
31
+ description = description.to_s.strip
32
+ [type_string.to_s.strip, description.empty? ? nil : description]
33
+ end
34
+
35
+ # The lines of each overload: a line that starts another overload begins a new group.
36
+ # @rbs (String? text) -> Array[Array[String]]
37
+ def self.groups(text)
38
+ method_type = false
39
+ "#{text}\n".lines.map(&:strip).slice_before do |line|
40
+ starts_overload = method_type && line.match?(OVERLOAD)
41
+ method_type ||= line.include?('->')
42
+ starts_overload
43
+ end.to_a
44
+ end
45
+ private_class_method :groups
46
+ end
47
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YardInlineRbs
4
+ # Writes the types declared in `.rbs` files onto the YARD objects of the same methods,
5
+ # attributes and constants. This runs after all files are parsed, and only adds the tags that
6
+ # are missing, so docstrings and inline annotations win.
7
+ class RbsFileTags
8
+ # @rbs @path: String
9
+
10
+ # The YARD path separators of each kind of RBS member.
11
+ SEPARATORS = {
12
+ instance: %w[#], singleton: %w[.], singleton_instance: %w[# .]
13
+ }.freeze #: Hash[Symbol, Array[String]]
14
+
15
+ # Moves the `.rbs` files before the Ruby files, in place. YARD replaces the whole docstring
16
+ # of an object whenever it parses a comment later, so the Ruby docstrings, with their
17
+ # description and inline annotations, must be parsed last. YARD has no option for the order.
18
+ # @rbs (Array[String] files) -> void
19
+ def self.parse_rbs_first(files)
20
+ files.replace(files.partition {|file| file.end_with?('.rbs')}.flatten)
21
+ end
22
+
23
+ # @rbs (Array[String] files) -> void
24
+ def self.apply(files)
25
+ files.grep(/\.rbs\z/).each {|file| new(file).apply}
26
+ end
27
+
28
+ # @rbs (String path) -> void
29
+ def initialize(path)
30
+ @path = path
31
+ end
32
+
33
+ # @rbs () -> void
34
+ def apply
35
+ declarations.each {|declaration| apply_declaration(declaration, '')}
36
+ end
37
+
38
+ private
39
+
40
+ # A file that cannot be read or parsed is skipped with a warning, so it does not stop the
41
+ # documentation of the other files. Only this step is rescued, so a bug in the tagging is not.
42
+ # @rbs () -> Array[RBS::AST::Declarations::t]
43
+ def declarations
44
+ buffer = RBS::Buffer.new(name: Pathname(@path), content: File.read(@path))
45
+ RBS::Parser.parse_signature(buffer).last
46
+ rescue RBS::ParsingError, ArgumentError, SystemCallError => e
47
+ YardInlineRbs.warn("#{@path}: #{e.message}")
48
+ []
49
+ end
50
+
51
+ # @rbs (RBS::AST::Declarations::t | RBS::AST::Members::t declaration, String namespace)
52
+ # -> void
53
+ def apply_declaration(declaration, namespace)
54
+ case declaration
55
+ when RBS::AST::Declarations::Class, RBS::AST::Declarations::Module
56
+ apply_members(declaration.members, qualify(namespace, declaration.name))
57
+ when RBS::AST::Declarations::Constant
58
+ tag_object(qualify(namespace, declaration.name)) {|tags| tags.type(declaration.type)}
59
+ end
60
+ end
61
+
62
+ # @rbs (Array[RBS::AST::Members::t | RBS::AST::Declarations::t] members, String namespace)
63
+ # -> void
64
+ def apply_members(members, namespace)
65
+ members.each do |member|
66
+ case member
67
+ when RBS::AST::Members::MethodDefinition then apply_method(member, namespace)
68
+ when RBS::AST::Members::AttrReader, RBS::AST::Members::AttrWriter,
69
+ RBS::AST::Members::AttrAccessor then apply_attribute(member, namespace)
70
+ else apply_declaration(member, namespace)
71
+ end
72
+ end
73
+ end
74
+
75
+ # @rbs (RBS::AST::Members::MethodDefinition member, String namespace) -> void
76
+ def apply_method(member, namespace)
77
+ method_types = member.overloads.map(&:method_type)
78
+ tag_members(namespace, member.name.to_s, member.kind) {|tags| tags.method_types(method_types)}
79
+ end
80
+
81
+ # @rbs (RBS::AST::Members::AttrReader | RBS::AST::Members::AttrWriter |
82
+ # RBS::AST::Members::AttrAccessor member, String namespace) -> void
83
+ def apply_attribute(member, namespace)
84
+ attribute_names(member).each do |name|
85
+ tag_members(namespace, name, member.kind) {|tags| tags.type(member.type)}
86
+ end
87
+ end
88
+
89
+ # @rbs (RBS::AST::Members::AttrReader | RBS::AST::Members::AttrWriter |
90
+ # RBS::AST::Members::AttrAccessor member) -> Array[String]
91
+ def attribute_names(member)
92
+ name = member.name.to_s
93
+ case member
94
+ when RBS::AST::Members::AttrReader then [name]
95
+ when RBS::AST::Members::AttrWriter then ["#{name}="]
96
+ else [name, "#{name}="]
97
+ end
98
+ end
99
+
100
+ # @rbs (String namespace, String name, Symbol kind) { (MemberTags) -> void } -> void
101
+ def tag_members(namespace, name, kind, &)
102
+ SEPARATORS.fetch(kind).each {|separator| tag_object("#{namespace}#{separator}#{name}", &)}
103
+ end
104
+
105
+ # @rbs (String path) { (MemberTags) -> void } -> void
106
+ def tag_object(path)
107
+ object = YARD::Registry.at(path)
108
+ return if object.nil?
109
+
110
+ member_tags = MemberTags.new(object)
111
+ yield member_tags
112
+ # Only an inline annotation makes a duplicate, so its location is known.
113
+ AnnotationWarnings.new(InlineTypes.location(object).to_s)
114
+ .report_rbs_file_duplicates(member_tags.duplicate_annotations, @path)
115
+ end
116
+
117
+ # @rbs (String namespace, RBS::TypeName name) -> String
118
+ def qualify(namespace, name)
119
+ return name.to_s.delete_prefix('::') if name.absolute?
120
+
121
+ [namespace, name.to_s].reject(&:empty?).join('::')
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YardInlineRbs
4
+ # The parameter and return types of an RBS function type, e.g. `(Integer x) -> bool`, as YARD
5
+ # type strings.
6
+ class RbsFunctionTypes
7
+ # @rbs @function: RBS::Types::Function
8
+
9
+ # @rbs (RBS::Types::Function function) -> void
10
+ def initialize(function)
11
+ @function = function
12
+ end
13
+
14
+ # @rbs () -> Array[PositionalParamType]
15
+ def positional_types
16
+ positionals.map do |param|
17
+ PositionalParamType.new(type: RbsTypeFormatter.format(param.type),
18
+ name: param.name&.to_s)
19
+ end
20
+ end
21
+
22
+ # An unnamed keyword rest is recorded under `ParameterNames::KEYWORD_REST_KEY`.
23
+ # @rbs () -> Hash[String, NamedParamAnnotation]
24
+ def named_types
25
+ rest = @function.rest_keywords
26
+ keywords.transform_values do |param|
27
+ NamedParamAnnotation.new(type: RbsTypeFormatter.format(param.type),
28
+ rest: param.equal?(rest))
29
+ end
30
+ end
31
+
32
+ # @rbs () -> Array[String]
33
+ def yield_types
34
+ positionals.map {|param| RbsTypeFormatter.format(param.type)}
35
+ end
36
+
37
+ # @rbs () -> String
38
+ def return_type
39
+ RbsTypeFormatter.format(@function.return_type)
40
+ end
41
+
42
+ private
43
+
44
+ # Ruby order: required, optional, rest, trailing.
45
+ # @rbs () -> Array[RBS::Types::Function::Param]
46
+ def positionals
47
+ @function.required_positionals + @function.optional_positionals +
48
+ [@function.rest_positionals].compact + @function.trailing_positionals
49
+ end
50
+
51
+ # @rbs () -> Hash[String, RBS::Types::Function::Param]
52
+ def keywords
53
+ result = @function.required_keywords.merge(@function.optional_keywords)
54
+ .transform_keys(&:to_s)
55
+ rest = @function.rest_keywords
56
+ result[rest.name&.to_s || ParameterNames::KEYWORD_REST_KEY] = rest if rest
57
+ result
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YardInlineRbs
4
+ # Changes to YARD's handlers for `.rbs` files. They write tags from the signature text: unnamed
5
+ # parameters get an empty name, generic types keep their RBS spelling, and the tags are lost
6
+ # when a Ruby docstring is parsed afterwards. RbsFileTags writes the tags once all files are
7
+ # parsed, so the method handler skips them, and the other handlers convert types with
8
+ # RbsTypeFormatter. There are two patches because the attribute and constant handlers write
9
+ # their tags inline, so only the type conversion can change them. Both rely on internals of
10
+ # YARD 0.9.45; the spec of `.install` fails when a YARD upgrade renames them.
11
+ module RbsHandlerPatches
12
+ # Applies both patches. The prepends live in a method, because YARD would read them as mixins
13
+ # of an unknown class when it documents this gem.
14
+ # @rbs () -> void
15
+ def self.install
16
+ YARD::Handlers::RBS::MethodHandler.prepend(MethodTags)
17
+ YARD::Handlers::RBS::MethodHandler.singleton_class.prepend(TypeConversion)
18
+ end
19
+
20
+ # Prepended to YARD::Handlers::RBS::MethodHandler.
21
+ module MethodTags
22
+ private
23
+
24
+ # @rbs (YARD::CodeObjects::MethodObject _object, Array[String]? _signatures) -> void
25
+ def apply_signature_tags(_object, _signatures); end
26
+ end
27
+
28
+ # Prepended to the singleton class of YARD::Handlers::RBS::MethodHandler.
29
+ module TypeConversion
30
+ # @rbs (String rbs) -> Array[String]
31
+ def rbs_type_to_yard_types(rbs)
32
+ rbs_type = RBS::Parser.parse_type(rbs, require_eof: true)
33
+ return super if rbs_type.nil?
34
+
35
+ [RbsTypeFormatter.format(rbs_type)]
36
+ rescue RBS::ParsingError
37
+ super
38
+ end
39
+ end
40
+ end
41
+ end