docscribe 1.1.0 → 1.2.1

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +662 -187
  3. data/exe/docscribe +2 -126
  4. data/lib/docscribe/cli/config_builder.rb +62 -0
  5. data/lib/docscribe/cli/init.rb +58 -0
  6. data/lib/docscribe/cli/options.rb +204 -0
  7. data/lib/docscribe/cli/run.rb +415 -0
  8. data/lib/docscribe/cli.rb +31 -0
  9. data/lib/docscribe/config/defaults.rb +71 -0
  10. data/lib/docscribe/config/emit.rb +142 -0
  11. data/lib/docscribe/config/filtering.rb +160 -0
  12. data/lib/docscribe/config/loader.rb +59 -0
  13. data/lib/docscribe/config/rbs.rb +51 -0
  14. data/lib/docscribe/config/sorbet.rb +87 -0
  15. data/lib/docscribe/config/sorting.rb +23 -0
  16. data/lib/docscribe/config/template.rb +184 -0
  17. data/lib/docscribe/config/utils.rb +102 -0
  18. data/lib/docscribe/config.rb +20 -230
  19. data/lib/docscribe/infer/ast_walk.rb +28 -0
  20. data/lib/docscribe/infer/constants.rb +11 -0
  21. data/lib/docscribe/infer/literals.rb +55 -0
  22. data/lib/docscribe/infer/names.rb +43 -0
  23. data/lib/docscribe/infer/params.rb +62 -0
  24. data/lib/docscribe/infer/raises.rb +68 -0
  25. data/lib/docscribe/infer/returns.rb +171 -0
  26. data/lib/docscribe/infer.rb +104 -258
  27. data/lib/docscribe/inline_rewriter/collector.rb +845 -0
  28. data/lib/docscribe/inline_rewriter/doc_block.rb +383 -0
  29. data/lib/docscribe/inline_rewriter/doc_builder.rb +607 -0
  30. data/lib/docscribe/inline_rewriter/source_helpers.rb +228 -0
  31. data/lib/docscribe/inline_rewriter/tag_sorter.rb +244 -0
  32. data/lib/docscribe/inline_rewriter.rb +599 -428
  33. data/lib/docscribe/parsing.rb +55 -44
  34. data/lib/docscribe/types/provider_chain.rb +37 -0
  35. data/lib/docscribe/types/rbs/provider.rb +213 -0
  36. data/lib/docscribe/types/rbs/type_formatter.rb +132 -0
  37. data/lib/docscribe/types/signature.rb +65 -0
  38. data/lib/docscribe/types/sorbet/base_provider.rb +217 -0
  39. data/lib/docscribe/types/sorbet/rbi_provider.rb +35 -0
  40. data/lib/docscribe/types/sorbet/source_provider.rb +25 -0
  41. data/lib/docscribe/version.rb +1 -1
  42. metadata +37 -3
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Infer
5
+ # Return type inference and rescue-conditional return extraction.
6
+ module Returns
7
+ module_function
8
+
9
+ # Infer a return type from a full method definition source string.
10
+ #
11
+ # The source must parse to a `:def` or `:defs` node. If parsing fails or inference
12
+ # is uncertain, the fallback type is returned.
13
+ #
14
+ # @note module_function: when included, also defines #infer_return_type (instance visibility: private)
15
+ # @param [String, nil] method_source full method definition source
16
+ # @raise [Parser::SyntaxError]
17
+ # @return [String]
18
+ def infer_return_type(method_source)
19
+ return FALLBACK_TYPE if method_source.nil? || method_source.strip.empty?
20
+
21
+ buffer = Parser::Source::Buffer.new('(method)')
22
+ buffer.source = method_source
23
+ root = Docscribe::Parsing.parse_buffer(buffer)
24
+ return FALLBACK_TYPE unless root && %i[def defs].include?(root.type)
25
+
26
+ body = root.children.last
27
+ last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true) || FALLBACK_TYPE
28
+ rescue Parser::SyntaxError
29
+ FALLBACK_TYPE
30
+ end
31
+
32
+ # Infer a method's normal return type from an already parsed def/defs node.
33
+ #
34
+ # @note module_function: when included, also defines #infer_return_type_from_node (instance visibility: private)
35
+ # @param [Parser::AST::Node] node `:def` or `:defs` node
36
+ # @return [String]
37
+ def infer_return_type_from_node(node)
38
+ body =
39
+ case node.type
40
+ when :def then node.children[2]
41
+ when :defs then node.children[3]
42
+ end
43
+
44
+ return FALLBACK_TYPE unless body
45
+
46
+ last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true) || FALLBACK_TYPE
47
+ end
48
+
49
+ # Return a structured return-type spec for a method node.
50
+ #
51
+ # The result includes:
52
+ # - `:normal` => normal/happy-path return type
53
+ # - `:rescues` => array of `[exception_names, return_type]` pairs for rescue branches
54
+ #
55
+ # @note module_function: when included, also defines #returns_spec_from_node (instance visibility: private)
56
+ # @param [Parser::AST::Node] node `:def` or `:defs` node
57
+ # @param [String] fallback_type type used when inference is uncertain
58
+ # @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
59
+ # @return [Hash]
60
+ def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true)
61
+ body =
62
+ case node.type
63
+ when :def then node.children[2]
64
+ when :defs then node.children[3]
65
+ end
66
+
67
+ spec = { normal: FALLBACK_TYPE, rescues: [] }
68
+ return spec unless body
69
+
70
+ if body.type == :rescue
71
+ main_body = body.children[0]
72
+ spec[:normal] =
73
+ last_expr_type(main_body, fallback_type: fallback_type, nil_as_optional: nil_as_optional) || FALLBACK_TYPE
74
+
75
+ body.children.each do |ch|
76
+ next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody
77
+
78
+ exc_list, _asgn, rescue_body = *ch
79
+ exc_names = Raises.exception_names_from_rescue_list(exc_list)
80
+ rtype =
81
+ last_expr_type(rescue_body, fallback_type: fallback_type, nil_as_optional: nil_as_optional) ||
82
+ fallback_type
83
+ spec[:rescues] << [exc_names, rtype]
84
+ end
85
+ else
86
+ spec[:normal] =
87
+ last_expr_type(body, fallback_type: fallback_type, nil_as_optional: nil_as_optional) || FALLBACK_TYPE
88
+ end
89
+
90
+ spec
91
+ end
92
+
93
+ # Infer the type of the last expression in a node.
94
+ #
95
+ # Supports:
96
+ # - `begin` groups
97
+ # - `if` branches
98
+ # - `case` expressions
99
+ # - explicit `return`
100
+ # - literal-like expressions via {Literals.type_from_literal}
101
+ #
102
+ # @note module_function: when included, also defines #last_expr_type (instance visibility: private)
103
+ # @param [Parser::AST::Node, nil] node expression node
104
+ # @param [String] fallback_type type used when inference is uncertain
105
+ # @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
106
+ # @return [String, nil]
107
+ def last_expr_type(node, fallback_type:, nil_as_optional:)
108
+ return nil unless node
109
+
110
+ case node.type
111
+ when :begin
112
+ last_expr_type(node.children.last, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
113
+
114
+ when :if
115
+ t = last_expr_type(node.children[1], fallback_type: fallback_type, nil_as_optional: nil_as_optional)
116
+ e = last_expr_type(node.children[2], fallback_type: fallback_type, nil_as_optional: nil_as_optional)
117
+ unify_types(t, e, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
118
+
119
+ when :case
120
+ branches = node.children[1..].compact.flat_map do |child|
121
+ if child.type == :when
122
+ last_expr_type(child.children.last, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
123
+ else
124
+ last_expr_type(child, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
125
+ end
126
+ end.compact
127
+
128
+ if branches.empty?
129
+ fallback_type
130
+ else
131
+ branches.reduce do |a, b|
132
+ unify_types(a, b, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
133
+ end
134
+ end
135
+
136
+ when :return
137
+ Literals.type_from_literal(node.children.first, fallback_type: fallback_type)
138
+
139
+ else
140
+ Literals.type_from_literal(node, fallback_type: fallback_type)
141
+ end
142
+ end
143
+
144
+ # Unify two inferred types into a single type string.
145
+ #
146
+ # Rules:
147
+ # - identical types remain unchanged
148
+ # - `nil` unions may become optional types if enabled
149
+ # - otherwise falls back conservatively to `fallback_type`
150
+ #
151
+ # @note module_function: when included, also defines #unify_types (instance visibility: private)
152
+ # @param [String, nil] a
153
+ # @param [String, nil] b
154
+ # @param [String] fallback_type
155
+ # @param [Boolean] nil_as_optional
156
+ # @return [String, nil]
157
+ def unify_types(a, b, fallback_type:, nil_as_optional:)
158
+ a ||= fallback_type
159
+ b ||= fallback_type
160
+ return a if a == b
161
+
162
+ if a == 'nil' || b == 'nil'
163
+ non_nil = (a == 'nil' ? b : a)
164
+ return nil_as_optional ? "#{non_nil}?" : "#{non_nil}, nil"
165
+ end
166
+
167
+ fallback_type
168
+ end
169
+ end
170
+ end
171
+ end
@@ -1,306 +1,152 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # NOTE: parser/base references Racc::Parser in some environments, so require runtime first.
3
4
  require 'racc/parser'
4
5
  require 'ast'
5
6
  require 'parser/ast/node'
6
7
  require 'parser/source/buffer'
7
8
  require 'parser/base'
9
+
8
10
  require 'docscribe/parsing'
9
11
 
12
+ require_relative 'infer/constants'
13
+ require_relative 'infer/ast_walk'
14
+ require_relative 'infer/names'
15
+ require_relative 'infer/literals'
16
+ require_relative 'infer/params'
17
+ require_relative 'infer/returns'
18
+ require_relative 'infer/raises'
19
+
10
20
  module Docscribe
21
+ # Best-effort inference utilities used to generate YARD tags.
22
+ #
23
+ # This module is intentionally heuristic:
24
+ # - it aims to be useful for common Ruby patterns
25
+ # - it prefers safe fallback behavior when uncertain
26
+ # - when inference cannot be specific, it falls back to `Object`
27
+ #
28
+ # External signature sources such as RBS and Sorbet are applied later in the
29
+ # doc builder and can override these inferred types.
11
30
  module Infer
12
31
  class << self
13
- # +Docscribe::Infer.infer_raises_from_node+ -> Object
14
- #
15
- # Method documentation.
32
+ # Infer exception classes raised or rescued within an AST node.
16
33
  #
17
- # @param [Object] node Param documentation.
18
- # @return [Object]
34
+ # @param [Parser::AST::Node] node
35
+ # @return [Array<String>]
19
36
  def infer_raises_from_node(node)
20
- raises = []
21
- walk = lambda do |n|
22
- return unless n.is_a?(Parser::AST::Node)
23
-
24
- case n.type
25
- when :rescue
26
- n.children.each { |ch| walk.call(ch) }
27
- when :resbody
28
- exc_list = n.children[0]
29
- if exc_list.nil?
30
- raises << 'StandardError'
31
- elsif exc_list.type == :array
32
- exc_list.children.each { |e| (c = const_full_name(e)) && (raises << c) }
33
- else
34
- (c = const_full_name(exc_list)) && (raises << c)
35
- end
36
- n.children.each { |ch| walk.call(ch) if ch.is_a?(Parser::AST::Node) }
37
- when :send
38
- recv, meth, *args = *n
39
- if recv.nil? && %i[raise fail].include?(meth)
40
- if args.empty?
41
- raises << 'StandardError'
42
- else
43
- c = const_full_name(args[0])
44
- raises << (c || 'StandardError')
45
- end
46
- end
47
- n.children.each { |ch| walk.call(ch) if ch.is_a?(Parser::AST::Node) }
48
- else
49
- n.children.each { |ch| walk.call(ch) if ch.is_a?(Parser::AST::Node) }
50
- end
51
- end
52
- walk.call(node)
53
- raises.uniq
37
+ Raises.infer_raises_from_node(node)
54
38
  end
55
39
 
56
- # +Docscribe::Infer.infer_param_type+ -> Object
40
+ # Infer a parameter type from its internal name form and optional default
41
+ # expression.
57
42
  #
58
- # Method documentation.
43
+ # The internal parameter name may include:
44
+ # - `*` for rest args
45
+ # - `**` for keyword rest args
46
+ # - `&` for block args
47
+ # - trailing `:` for keyword args
59
48
  #
60
- # @param [Object] name Param documentation.
61
- # @param [Object] default_str Param documentation.
62
- # @return [Object]
63
- def infer_param_type(name, default_str)
64
- # splats and kwargs are driven by name shape
65
- return 'Array' if name.start_with?('*') && !name.start_with?('**')
66
- return 'Hash' if name.start_with?('**')
67
- return 'Proc' if name.start_with?('&')
68
-
69
- # keyword arg e.g. "verbose:" — default_str might be nil or something
70
- is_kw = name.end_with?(':')
71
-
72
- node = parse_expr(default_str)
73
- ty = type_from_literal(node)
74
-
75
- # If kw with no default, still show Object (or Hash for options:)
76
- if is_kw && default_str.nil?
77
- return (name == 'options:' ? 'Hash' : 'Object')
78
- end
79
-
80
- # If param named options and default is {}, call it Hash
81
- return 'Hash' if name == 'options:' && (default_str == '{}' || ty == 'Hash')
82
-
83
- ty
49
+ # @param [String] name internal parameter name representation
50
+ # @param [String, nil] default_str source for the default expression
51
+ # @param [String] fallback_type
52
+ # @param [Boolean] treat_options_keyword_as_hash
53
+ # @return [String]
54
+ def infer_param_type(name, default_str, fallback_type: FALLBACK_TYPE, treat_options_keyword_as_hash: true)
55
+ Params.infer_param_type(
56
+ name,
57
+ default_str,
58
+ fallback_type: fallback_type,
59
+ treat_options_keyword_as_hash: treat_options_keyword_as_hash
60
+ )
84
61
  end
85
62
 
86
- # +Docscribe::Infer.parse_expr+ -> Object
63
+ # Parse a standalone expression source string for inference helpers.
87
64
  #
88
- # Method documentation.
89
- #
90
- # @param [Object] src Param documentation.
91
- # @raise [Parser::SyntaxError]
92
- # @return [Object]
93
- # @return [nil] if Parser::SyntaxError
65
+ # @param [String, nil] src
66
+ # @return [Parser::AST::Node, nil]
94
67
  def parse_expr(src)
95
- return nil if src.nil? || src.strip.empty?
96
-
97
- buffer = Parser::Source::Buffer.new('(param)')
98
- buffer.source = src
99
- Docscribe::Parsing.parse_buffer(buffer)
100
- rescue Parser::SyntaxError
101
- nil
68
+ Params.parse_expr(src)
102
69
  end
103
70
 
104
- # +Docscribe::Infer.infer_return_type+ -> Object
105
- #
106
- # Method documentation.
71
+ # Infer a return type from full method source.
107
72
  #
108
- # @param [Object] method_source Param documentation.
109
- # @raise [Parser::SyntaxError]
110
- # @return [Object]
111
- # @return [String] if Parser::SyntaxError
73
+ # @param [String, nil] method_source
74
+ # @return [String]
112
75
  def infer_return_type(method_source)
113
- return 'Object' if method_source.nil? || method_source.strip.empty?
114
-
115
- buffer = Parser::Source::Buffer.new('(method)')
116
- buffer.source = method_source
117
- root = Docscribe::Parsing.parse_buffer(buffer)
118
- return 'Object' unless root && %i[def defs].include?(root.type)
119
-
120
- body = root.children.last # method body node
121
- ty = last_expr_type(body)
122
- ty || 'Object'
123
- rescue Parser::SyntaxError
124
- 'Object'
76
+ Returns.infer_return_type(method_source)
125
77
  end
126
78
 
127
- # +Docscribe::Infer.infer_return_type_from_node+ -> Object
128
- #
129
- # Method documentation.
79
+ # Infer a return type from an already parsed `:def` / `:defs` node.
130
80
  #
131
- # @param [Object] node Param documentation.
132
- # @return [Object]
81
+ # @param [Parser::AST::Node] node
82
+ # @return [String]
133
83
  def infer_return_type_from_node(node)
134
- body =
135
- case node.type
136
- when :def then node.children[2] # [name, args, body]
137
- when :defs then node.children[3] # [recv, name, args, body]
138
- end
139
- return 'Object' unless body
140
-
141
- ty = last_expr_type(body)
142
- ty || 'Object'
84
+ Returns.infer_return_type_from_node(node)
143
85
  end
144
86
 
145
- # +Docscribe::Infer.returns_spec_from_node+ -> Object
146
- #
147
- # Method documentation.
148
- #
149
- # @param [Object] node Param documentation.
150
- # @return [Object]
151
- def returns_spec_from_node(node)
152
- # Returns a Hash like: { normal: 'Type', rescues: [[['Foo','Bar'], 'Type'], ...] }
153
- body =
154
- case node.type
155
- when :def then node.children[2] # [name, args, body]
156
- when :defs then node.children[3] # [recv, name, args, body]
157
- end
158
-
159
- spec = { normal: 'Object', rescues: [] }
160
- return spec unless body
161
-
162
- if body.type == :rescue
163
- # child[0] is the main body (before rescue)
164
- main_body = body.children[0]
165
- spec[:normal] = last_expr_type(main_body) || 'Object'
166
-
167
- # :resbody nodes hold exception list, optional var, and rescue body
168
- body.children.each do |ch|
169
- next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody
170
-
171
- exc_list, _asgn, rescue_body = *ch
172
-
173
- exc_names = []
174
- if exc_list.nil?
175
- exc_names << 'StandardError'
176
- elsif exc_list.type == :array
177
- exc_list.children.each do |e|
178
- name = const_full_name(e)
179
- exc_names << (name || 'StandardError')
180
- end
181
- else
182
- name = const_full_name(exc_list)
183
- exc_names << (name || 'StandardError')
184
- end
185
-
186
- rtype = last_expr_type(rescue_body) || 'Object'
187
- spec[:rescues] << [exc_names, rtype]
188
- end
189
- else
190
- spec[:normal] = last_expr_type(body) || 'Object'
191
- end
192
-
193
- spec
87
+ # Return structured normal/rescue return information for a method node.
88
+ #
89
+ # Result shape:
90
+ # - `:normal` => the normal return type
91
+ # - `:rescues` => rescue-branch conditional return info
92
+ #
93
+ # @param [Parser::AST::Node] node
94
+ # @param [String] fallback_type
95
+ # @param [Boolean] nil_as_optional
96
+ # @return [Hash]
97
+ def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true)
98
+ Returns.returns_spec_from_node(
99
+ node,
100
+ fallback_type: fallback_type,
101
+ nil_as_optional: nil_as_optional
102
+ )
194
103
  end
195
104
 
196
- # +Docscribe::Infer.last_expr_type+ -> Object
197
- #
198
- # Method documentation.
199
- #
200
- # @param [Object] node Param documentation.
201
- # @return [Object]
202
- def last_expr_type(node)
203
- return nil unless node
204
-
205
- case node.type
206
- when :begin
207
- last = node.children.last
208
- last_expr_type(last)
209
- when :if
210
- t = last_expr_type(node.children[1])
211
- e = last_expr_type(node.children[2])
212
- unify_types(t, e)
213
- when :case
214
- # check whens and else
215
- branches = node.children[1..].compact.flat_map do |child|
216
- if child && child.type == :when
217
- last_expr_type(child.children.last)
218
- else
219
- last_expr_type(child)
220
- end
221
- end
222
- branches.compact!
223
- branches.empty? ? 'Object' : branches.reduce { |a, b| unify_types(a, b) }
224
- when :return
225
- type_from_literal(node.children.first)
226
- else
227
- type_from_literal(node)
228
- end
105
+ # Infer the type of the last expression in an AST node.
106
+ #
107
+ # @param [Parser::AST::Node, nil] node
108
+ # @param [String] fallback_type
109
+ # @param [Boolean] nil_as_optional
110
+ # @return [String, nil]
111
+ def last_expr_type(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true)
112
+ Returns.last_expr_type(
113
+ node,
114
+ fallback_type: fallback_type,
115
+ nil_as_optional: nil_as_optional
116
+ )
229
117
  end
230
118
 
231
- # +Docscribe::Infer.const_full_name+ -> Object
232
- #
233
- # Method documentation.
119
+ # Convert a constant AST node into its fully qualified name.
234
120
  #
235
- # @param [Object] n Param documentation.
236
- # @return [Object]
121
+ # @param [Parser::AST::Node, nil] n
122
+ # @return [String, nil]
237
123
  def const_full_name(n)
238
- return nil unless n.is_a?(Parser::AST::Node)
239
-
240
- case n.type
241
- when :const
242
- scope, name = *n
243
- scope_name = const_full_name(scope)
244
- if scope_name && !scope_name.empty?
245
- "#{scope_name}::#{name}"
246
- elsif scope_name == '' # leading ::
247
- "::#{name}"
248
- else
249
- name.to_s
250
- end
251
- when :cbase
252
- '' # represents leading :: scope
253
- end
124
+ Names.const_full_name(n)
254
125
  end
255
126
 
256
- # +Docscribe::Infer.type_from_literal+ -> Object
257
- #
258
- # Method documentation.
127
+ # Infer a YARD-ish type string from a literal AST node.
259
128
  #
260
- # @param [Object] node Param documentation.
261
- # @return [Object]
262
- def type_from_literal(node)
263
- return 'Object' unless node
264
-
265
- case node.type
266
- when :int then 'Integer'
267
- when :float then 'Float'
268
- when :str, :dstr then 'String'
269
- when :sym then 'Symbol'
270
- when :true, :false then 'Boolean' # rubocop:disable Lint/BooleanSymbol
271
- when :nil then 'nil'
272
- when :array then 'Array'
273
- when :hash then 'Hash'
274
- when :regexp then 'Regexp'
275
- when :const
276
- node.children.last.to_s
277
- when :send
278
- recv, meth, = node.children
279
- if meth == :new && recv && recv.type == :const
280
- recv.children.last.to_s
281
- else
282
- 'Object'
283
- end
284
- else
285
- 'Object'
286
- end
129
+ # @param [Parser::AST::Node, nil] node
130
+ # @param [String] fallback_type
131
+ # @return [String]
132
+ def type_from_literal(node, fallback_type: FALLBACK_TYPE)
133
+ Literals.type_from_literal(node, fallback_type: fallback_type)
287
134
  end
288
135
 
289
- # +Docscribe::Infer.unify_types+ -> String
136
+ # Unify two inferred type strings conservatively.
290
137
  #
291
- # Method documentation.
292
- #
293
- # @param [Object] a Param documentation.
294
- # @param [Object] b Param documentation.
138
+ # @param [String, nil] a
139
+ # @param [String, nil] b
140
+ # @param [String] fallback_type
141
+ # @param [Boolean] nil_as_optional
295
142
  # @return [String]
296
- def unify_types(a, b)
297
- a ||= 'Object'
298
- b ||= 'Object'
299
- return a if a == b
300
- # nil-union => Optional
301
- return "#{a == 'nil' ? b : a}?" if a == 'nil' || b == 'nil'
302
-
303
- 'Object'
143
+ def unify_types(a, b, fallback_type: FALLBACK_TYPE, nil_as_optional: true)
144
+ Returns.unify_types(
145
+ a,
146
+ b,
147
+ fallback_type: fallback_type,
148
+ nil_as_optional: nil_as_optional
149
+ )
304
150
  end
305
151
  end
306
152
  end