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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE +21 -0
- data/README.md +174 -0
- data/lib/yard-inline-rbs.rb +32 -0
- data/lib/yard_inline_rbs/annotation_warnings.rb +58 -0
- data/lib/yard_inline_rbs/declaration_source.rb +115 -0
- data/lib/yard_inline_rbs/inline_annotations.rb +63 -0
- data/lib/yard_inline_rbs/inline_types.rb +39 -0
- data/lib/yard_inline_rbs/member_tags.rb +43 -0
- data/lib/yard_inline_rbs/object_tags.rb +26 -0
- data/lib/yard_inline_rbs/overload_text.rb +65 -0
- data/lib/yard_inline_rbs/parameter_names.rb +65 -0
- data/lib/yard_inline_rbs/parse_result.rb +177 -0
- data/lib/yard_inline_rbs/plugin.rb +24 -0
- data/lib/yard_inline_rbs/rbs_comment_parser.rb +143 -0
- data/lib/yard_inline_rbs/rbs_description.rb +47 -0
- data/lib/yard_inline_rbs/rbs_file_tags.rb +124 -0
- data/lib/yard_inline_rbs/rbs_function_types.rb +60 -0
- data/lib/yard_inline_rbs/rbs_handler_patches.rb +41 -0
- data/lib/yard_inline_rbs/rbs_type_formatter.rb +114 -0
- data/lib/yard_inline_rbs/result_applier.rb +170 -0
- data/lib/yard_inline_rbs/tag_injector.rb +125 -0
- data/lib/yard_inline_rbs/type_parser.rb +106 -0
- metadata +97 -0
|
@@ -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
|