docscribe 1.2.1 → 1.3.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.
@@ -11,6 +11,7 @@ module Docscribe
11
11
  # @return [Docscribe::Types::RBS::Provider, nil]
12
12
  def rbs_provider
13
13
  return nil unless rbs_enabled?
14
+ return nil unless ruby_supports_rbs?
14
15
 
15
16
  @rbs_provider ||= begin
16
17
  require 'docscribe/types/rbs/provider'
@@ -30,8 +31,43 @@ module Docscribe
30
31
  fetch_bool(%w[rbs enabled], false)
31
32
  end
32
33
 
34
+ # Method documentation.
35
+ #
36
+ # @raise [LoadError]
37
+ # @return [Object]
38
+ def core_rbs_provider
39
+ return nil unless ruby_supports_rbs?
40
+
41
+ @core_rbs_provider ||= begin
42
+ require 'docscribe/types/rbs/provider'
43
+ Docscribe::Types::RBS::Provider.new(
44
+ sig_dirs: [],
45
+ collapse_generics: false
46
+ )
47
+ rescue LoadError
48
+ nil
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # Method documentation.
55
+ #
56
+ # @private
57
+ # @return [Boolean]
58
+ def ruby_supports_rbs?
59
+ return true if RUBY_VERSION >= '3.0'
60
+
61
+ @rbs_warning_emitted ||= begin
62
+ warn 'Docscribe: RBS requires Ruby 3.0+. Falling back to inference.'
63
+ true
64
+ end
65
+ false
66
+ end
67
+
33
68
  # Signature directories used by the RBS provider.
34
69
  #
70
+ # @private
35
71
  # @return [Array<String>]
36
72
  def rbs_sig_dirs
37
73
  Array(raw.dig('rbs', 'sig_dirs') || DEFAULT.dig('rbs', 'sig_dirs')).map(&:to_s)
@@ -43,7 +79,8 @@ module Docscribe
43
79
  # - `Hash<Symbol, String>` => `Hash`
44
80
  # - `Array<Integer>` => `Array`
45
81
  #
46
- # @return [Boolean]
82
+ # @private
83
+ # @return [Object]
47
84
  def rbs_collapse_generics?
48
85
  fetch_bool(%w[rbs collapse_generics], false)
49
86
  end
@@ -14,170 +14,94 @@ module Docscribe
14
14
  ---
15
15
  # Docscribe configuration file
16
16
  #
17
- # Inspect what safe doc updates would be applied:
18
- # bundle exec docscribe lib
19
- #
20
- # Apply safe doc updates:
21
- # bundle exec docscribe -a lib
22
- #
23
- # Apply aggressive doc updates (rebuild existing doc blocks):
24
- # bundle exec docscribe -A lib
17
+ # Docscribe works without this file create it only for customization.
25
18
  #
19
+ # Quick start:
20
+ # bundle exec docscribe lib # check what would change
21
+ # bundle exec docscribe -a lib # apply safe updates
22
+ # bundle exec docscribe -A lib # rebuild all doc blocks
26
23
 
27
24
  emit:
28
- # Emit the header line:
29
- #
30
- # +MyClass#my_method+ -> ReturnType
31
- header: false
32
-
33
- # Whether to include the default placeholder line:
34
- # # Method documentation.
35
- include_default_message: true
36
-
37
- # Whether to append placeholder text to generated @param tags:
38
- # # @param [String] name Param documentation.
39
- include_param_documentation: true
40
-
41
- # Emit @param tags.
42
- param_tags: true
43
-
44
- # Emit @return tag (can be overridden per scope/visibility under methods:).
45
- return_tag: true
46
-
47
- # Emit @private / @protected tags based on Ruby visibility context.
48
- visibility_tags: true
49
-
50
- # Emit @raise tags inferred from rescue clauses / raise/fail calls.
51
- raise_tags: true
52
-
53
- # Emit conditional rescue return tags:
54
- #
55
- # @return [String] if FooError, BarError
56
- rescue_conditional_returns: true
57
-
58
- # Generate @!attribute docs for attr_reader/attr_writer/attr_accessor.
59
- attributes: false
25
+ # What to include in generated documentation
26
+ header: false # +MyClass#foo+ -> ReturnType
27
+ param_tags: true # @param tags
28
+ return_tag: true # @return tag
29
+ visibility_tags: true # @private / @protected
30
+ raise_tags: true # @raise tags
31
+ rescue_conditional_returns: true # @return [Type] if Error
32
+ attributes: false # @!attribute for attr_*
33
+
34
+ # Placeholder text for generated docs
35
+ include_default_message: true # "Method documentation."
36
+ include_param_documentation: true # "Param documentation."
60
37
 
61
38
  doc:
62
- # Default text inserted into each generated doc block.
39
+ # Default text and formatting
63
40
  default_message: "Method documentation."
64
-
65
- # Default text appended to generated @param tags.
66
41
  param_documentation: "Param documentation."
42
+ param_tag_style: "type_name" # "type_name" or "name_type"
43
+ sort_tags: true
44
+ tag_order: ["todo", "note", "api", "private", "protected", "param", "option", "yieldparam", "raise", "return"]
67
45
 
68
- # Style for generated @param tags:
69
- # - type_name => @param [Type] name
70
- # - name_type => @param name [Type]
71
- param_tag_style: "type_name"
46
+ inference:
47
+ # Type inference behavior
48
+ fallback_type: "Object" # when uncertain
49
+ nil_as_optional: true # String | nil => String?
50
+ treat_options_keyword_as_hash: true # options: keyword => Hash
72
51
 
73
- # Sort generated / merged tags in safe mode when possible.
74
- sort_tags: true
52
+ filter:
53
+ # Which methods and files to process
54
+ # Method format: "Container#method" (instance) or "Container.method" (class)
55
+ # Supports globs ("*#initialize") and regex ("/^MyApp::.*$/")
56
+ include: []
57
+ exclude: []
58
+ visibilities: ["public", "protected", "private"]
59
+ scopes: ["instance", "class"]
75
60
 
76
- # Tag order used when sorting contiguous tag runs.
77
- tag_order: ["todo", "note", "api", "private", "protected", "param", "option", "yieldparam", "raise", "return"]
61
+ files:
62
+ # File paths relative to project root (globs or /regex/)
63
+ include: []
64
+ exclude: ["spec"]
78
65
 
79
66
  methods:
80
- # Per-scope / per-visibility overrides.
67
+ # Override defaults per scope and visibility.
68
+ # Empty {} means "use values from `doc` section".
81
69
  #
82
70
  # Example:
83
- # methods:
84
71
  # instance:
85
72
  # public:
86
73
  # default_message: "Public API."
87
- # return_tag: true
74
+ # private:
75
+ # return_tag: false
88
76
  instance:
89
77
  public: {}
90
78
  protected: {}
91
79
  private: {}
92
-
93
80
  class:
94
81
  public: {}
95
82
  protected: {}
96
83
  private: {}
97
84
 
98
- inference:
99
- # Type used when inference is uncertain.
100
- fallback_type: "Object"
101
-
102
- # Whether nil unions become optional types (for example String | nil => String?).
103
- nil_as_optional: true
104
-
105
- # Special-case: treat keyword arg named options/options: as a Hash.
106
- treat_options_keyword_as_hash: true
107
-
108
- filter:
109
- # Filter which methods Docscribe touches.
110
- #
111
- # Method id format:
112
- # instance: "MyModule::MyClass#instance_method"
113
- # class: "MyModule::MyClass.class_method"
114
- #
115
- # Patterns:
116
- # - glob: "*#initialize", "MyApp::*#*"
117
- # - regex: "/^MyApp::.*#(foo|bar)$/"
118
- #
119
- # Semantics:
120
- # - scopes / visibilities act as allow-lists
121
- # - exclude wins
122
- # - if include is empty => include everything (subject to allow-lists)
123
- visibilities: ["public", "protected", "private"]
124
- scopes: ["instance", "class"]
125
- include: []
126
- exclude: []
127
-
128
- files:
129
- # Filter which files Docscribe processes (paths are matched relative
130
- # to the project root).
131
- #
132
- # Tips:
133
- # - Use directory shorthand to exclude a whole directory:
134
- # exclude: ["spec"]
135
- # - Or use globs:
136
- # exclude: ["spec/**/*.rb", "vendor/**/*.rb"]
137
- include: []
138
- exclude: ["spec"]
139
-
140
85
  rbs:
141
- # Optional: use RBS signatures to improve @param / @return types.
142
- #
143
- # CLI equivalent:
144
- # bundle exec docscribe -a --rbs --sig-dir sig lib
145
- #
146
- # Under Bundler, you may need `gem "rbs"` in your Gemfile (or a
147
- # Gemfile that includes it), otherwise `require "rbs"` may fail and
148
- # Docscribe will fall back to inference.
86
+ # Use RBS signatures for better types (requires `gem "rbs"`)
149
87
  enabled: false
150
-
151
- # Signature directories (repeatable via --sig-dir).
152
88
  sig_dirs: ["sig"]
153
-
154
- # If true, simplify generic types:
155
- # - Hash<Symbol, String> => Hash
156
- # - Array<Integer> => Array
157
- collapse_generics: false
89
+ collapse_generics: false # Hash<Symbol, String> => Hash
90
+ collection: false # auto-discover from rbs_collection.lock.yaml
158
91
 
159
92
  sorbet:
160
- # Optional: use Sorbet signatures from inline `sig` declarations and
161
- # RBI files to improve @param / @return types.
162
- #
163
- # CLI equivalent:
164
- # bundle exec docscribe -a --sorbet --rbi-dir sorbet/rbi lib
165
- #
166
- # Sorbet resolution order is:
167
- # 1. inline `sig` in the current source file
168
- # 2. RBI files
169
- # 3. RBS
170
- # 4. AST inference
93
+ # Use Sorbet inline sigs and RBI files for better types
171
94
  enabled: false
172
-
173
- # RBI directories scanned recursively for `.rbi` files
174
- # (repeatable via --rbi-dir).
175
95
  rbi_dirs: ["sorbet/rbi", "rbi"]
176
-
177
- # If true, simplify generic types:
178
- # - Hash<Symbol, String> => Hash
179
- # - Array<Integer> => Array
180
96
  collapse_generics: false
97
+
98
+ plugins:
99
+ # Load custom plugins
100
+ # Example:
101
+ # require:
102
+ # - ./docscribe_plugins
103
+ # - docscribe-rails-associations
104
+ require: []
181
105
  YAML
182
106
  end
183
107
  end
@@ -33,3 +33,4 @@ require_relative 'config/filtering'
33
33
  require_relative 'config/rbs'
34
34
  require_relative 'config/sorting'
35
35
  require_relative 'config/sorbet'
36
+ require_relative 'config/plugin'
@@ -24,7 +24,9 @@ module Docscribe
24
24
  return FALLBACK_TYPE unless root && %i[def defs].include?(root.type)
25
25
 
26
26
  body = root.children.last
27
- last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true) || FALLBACK_TYPE
27
+ local_var_types = build_local_variable_types(body)
28
+ last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
29
+ local_var_types: local_var_types) || FALLBACK_TYPE
28
30
  rescue Parser::SyntaxError
29
31
  FALLBACK_TYPE
30
32
  end
@@ -43,7 +45,9 @@ module Docscribe
43
45
 
44
46
  return FALLBACK_TYPE unless body
45
47
 
46
- last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true) || FALLBACK_TYPE
48
+ local_var_types = build_local_variable_types(body)
49
+ last_expr_type(body, fallback_type: FALLBACK_TYPE, nil_as_optional: true,
50
+ local_var_types: local_var_types) || FALLBACK_TYPE
47
51
  end
48
52
 
49
53
  # Return a structured return-type spec for a method node.
@@ -56,8 +60,11 @@ module Docscribe
56
60
  # @param [Parser::AST::Node] node `:def` or `:defs` node
57
61
  # @param [String] fallback_type type used when inference is uncertain
58
62
  # @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
63
+ # @param [nil] core_rbs_provider Param documentation.
64
+ # @param [nil] param_types Param documentation.
59
65
  # @return [Hash]
60
- def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true)
66
+ def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil,
67
+ param_types: nil)
61
68
  body =
62
69
  case node.type
63
70
  when :def then node.children[2]
@@ -67,10 +74,16 @@ module Docscribe
67
74
  spec = { normal: FALLBACK_TYPE, rescues: [] }
68
75
  return spec unless body
69
76
 
77
+ local_var_types = build_local_variable_types(body)
78
+
70
79
  if body.type == :rescue
71
80
  main_body = body.children[0]
81
+ rescue_local_var_types = build_local_variable_types(body)
82
+ all_local_var_types = rescue_local_var_types || local_var_types
72
83
  spec[:normal] =
73
- last_expr_type(main_body, fallback_type: fallback_type, nil_as_optional: nil_as_optional) || FALLBACK_TYPE
84
+ last_expr_type(main_body, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
85
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
86
+ local_var_types: all_local_var_types) || FALLBACK_TYPE
74
87
 
75
88
  body.children.each do |ch|
76
89
  next unless ch.is_a?(Parser::AST::Node) && ch.type == :resbody
@@ -78,18 +91,51 @@ module Docscribe
78
91
  exc_list, _asgn, rescue_body = *ch
79
92
  exc_names = Raises.exception_names_from_rescue_list(exc_list)
80
93
  rtype =
81
- last_expr_type(rescue_body, fallback_type: fallback_type, nil_as_optional: nil_as_optional) ||
94
+ last_expr_type(rescue_body, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
95
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
96
+ local_var_types: all_local_var_types) ||
82
97
  fallback_type
83
98
  spec[:rescues] << [exc_names, rtype]
84
99
  end
85
100
  else
86
101
  spec[:normal] =
87
- last_expr_type(body, fallback_type: fallback_type, nil_as_optional: nil_as_optional) || FALLBACK_TYPE
102
+ last_expr_type(body, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
103
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
104
+ local_var_types: local_var_types) || FALLBACK_TYPE
88
105
  end
89
106
 
90
107
  spec
91
108
  end
92
109
 
110
+ # Resolve a return type from core RBS for a method call.
111
+ #
112
+ # @note module_function: when included, also defines #resolve_rbs_return_type (instance visibility: private)
113
+ # @private
114
+ # @param [Object] node Param documentation.
115
+ # @return [String] FALLBACK_TYPE if lookup fails
116
+ def build_local_variable_types(node)
117
+ types = {}
118
+ ASTWalk.walk(node) do |n|
119
+ case n.type
120
+ when :lvasgn, :gvasgn, :ivasgn
121
+ name = n.children[0].to_s
122
+ value = n.children[1]
123
+ if value
124
+ inferred = Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
125
+ types[name] = inferred if inferred && inferred != FALLBACK_TYPE
126
+ end
127
+ when :casgn
128
+ name = n.children[0].to_s
129
+ value = n.children[2]
130
+ if value
131
+ inferred = Literals.type_from_literal(value, fallback_type: FALLBACK_TYPE)
132
+ types[name] = inferred if inferred && inferred != FALLBACK_TYPE
133
+ end
134
+ end
135
+ end
136
+ types.empty? ? nil : types
137
+ end
138
+
93
139
  # Infer the type of the last expression in a node.
94
140
  #
95
141
  # Supports:
@@ -98,30 +144,45 @@ module Docscribe
98
144
  # - `case` expressions
99
145
  # - explicit `return`
100
146
  # - literal-like expressions via {Literals.type_from_literal}
147
+ # - method calls with RBS core type lookup
101
148
  #
102
149
  # @note module_function: when included, also defines #last_expr_type (instance visibility: private)
103
150
  # @param [Parser::AST::Node, nil] node expression node
104
151
  # @param [String] fallback_type type used when inference is uncertain
105
152
  # @param [Boolean] nil_as_optional whether `nil` unions should be rendered as optional types
153
+ # @param [Object, nil] core_rbs_provider optional RBS provider for core type lookup
154
+ # @param [Hash, nil] param_types parameter name -> type map for lvar resolution
155
+ # @param [nil] local_var_types Param documentation.
106
156
  # @return [String, nil]
107
- def last_expr_type(node, fallback_type:, nil_as_optional:)
157
+ def last_expr_type(node, fallback_type:, nil_as_optional:, core_rbs_provider: nil, param_types: nil,
158
+ local_var_types: nil)
108
159
  return nil unless node
109
160
 
110
161
  case node.type
111
162
  when :begin
112
- last_expr_type(node.children.last, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
163
+ last_expr_type(node.children.last, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
164
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
165
+ local_var_types: local_var_types)
113
166
 
114
167
  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)
168
+ t = last_expr_type(node.children[1], fallback_type: fallback_type, nil_as_optional: nil_as_optional,
169
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
170
+ local_var_types: local_var_types)
171
+ e = last_expr_type(node.children[2], fallback_type: fallback_type, nil_as_optional: nil_as_optional,
172
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
173
+ local_var_types: local_var_types)
117
174
  unify_types(t, e, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
118
175
 
119
176
  when :case
120
177
  branches = node.children[1..].compact.flat_map do |child|
121
178
  if child.type == :when
122
- last_expr_type(child.children.last, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
179
+ last_expr_type(child.children.last, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
180
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
181
+ local_var_types: local_var_types)
123
182
  else
124
- last_expr_type(child, fallback_type: fallback_type, nil_as_optional: nil_as_optional)
183
+ last_expr_type(child, fallback_type: fallback_type, nil_as_optional: nil_as_optional,
184
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
185
+ local_var_types: local_var_types)
125
186
  end
126
187
  end.compact
127
188
 
@@ -136,11 +197,89 @@ module Docscribe
136
197
  when :return
137
198
  Literals.type_from_literal(node.children.first, fallback_type: fallback_type)
138
199
 
200
+ when :block
201
+ send_node = node.children[0]
202
+ if send_node&.type == :send
203
+ recv = send_node.children[0]
204
+ meth = send_node.children[1]
205
+
206
+ if core_rbs_provider && recv&.type == :lvar
207
+ lvar_name = recv.children.first
208
+ recv_type = nil
209
+ recv_type = local_var_types[lvar_name.to_s] if local_var_types && lvar_name
210
+ recv_type = param_types[lvar_name.to_s] if !recv_type && param_types && lvar_name
211
+ if recv_type
212
+ rbs_type = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
213
+ return rbs_type unless rbs_type == FALLBACK_TYPE
214
+ end
215
+ elsif core_rbs_provider && recv&.type == :send
216
+ inner_type = last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
217
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
218
+ local_var_types: local_var_types)
219
+ if inner_type
220
+ rbs_type = resolve_rbs_return_type(inner_type, meth, core_rbs_provider)
221
+ return rbs_type unless rbs_type == FALLBACK_TYPE
222
+ end
223
+ end
224
+ end
225
+
226
+ last_expr_type(node.children[2], fallback_type: fallback_type, nil_as_optional: nil_as_optional,
227
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
228
+ local_var_types: local_var_types)
229
+
230
+ when :send
231
+ recv = node.children[0]
232
+ meth = node.children[1]
233
+
234
+ # Try to resolve return type from RBS core for method calls
235
+ if core_rbs_provider && recv&.type == :send
236
+ # Chained call: arg.to_i.positive?
237
+ inner_type = last_expr_type(recv, fallback_type: nil, nil_as_optional: false,
238
+ core_rbs_provider: core_rbs_provider, param_types: param_types,
239
+ local_var_types: local_var_types)
240
+ if inner_type
241
+ rbs_type = resolve_rbs_return_type(inner_type, meth, core_rbs_provider)
242
+ return rbs_type unless rbs_type == FALLBACK_TYPE
243
+ end
244
+
245
+ elsif core_rbs_provider && recv&.type == :lvar
246
+ # Direct call on local variable: p1.positive? or admins.any?
247
+ lvar_name = recv.children.first
248
+ recv_type = nil
249
+ recv_type = local_var_types[lvar_name.to_s] if local_var_types && lvar_name
250
+ recv_type = param_types[lvar_name.to_s] if !recv_type && param_types && lvar_name
251
+ if recv_type
252
+ rbs_type = resolve_rbs_return_type(recv_type, meth, core_rbs_provider)
253
+ return rbs_type unless rbs_type == FALLBACK_TYPE
254
+ end
255
+ end
256
+
257
+ Literals.type_from_literal(node, fallback_type: fallback_type)
258
+
139
259
  else
140
260
  Literals.type_from_literal(node, fallback_type: fallback_type)
141
261
  end
142
262
  end
143
263
 
264
+ # Method documentation.
265
+ #
266
+ # @note module_function: when included, also defines #resolve_rbs_return_type (instance visibility: private)
267
+ # @param [Object] container_type Param documentation.
268
+ # @param [Object] method_name Param documentation.
269
+ # @param [Object] core_rbs_provider Param documentation.
270
+ # @return [Object]
271
+ def resolve_rbs_return_type(container_type, method_name, core_rbs_provider)
272
+ return FALLBACK_TYPE unless core_rbs_provider
273
+
274
+ sig = core_rbs_provider.signature_for(
275
+ container: container_type,
276
+ scope: :instance,
277
+ name: method_name
278
+ )
279
+
280
+ sig&.return_type || FALLBACK_TYPE
281
+ end
282
+
144
283
  # Unify two inferred types into a single type string.
145
284
  #
146
285
  # Rules:
@@ -93,12 +93,17 @@ module Docscribe
93
93
  # @param [Parser::AST::Node] node
94
94
  # @param [String] fallback_type
95
95
  # @param [Boolean] nil_as_optional
96
+ # @param [nil] core_rbs_provider Param documentation.
97
+ # @param [nil] param_types Param documentation.
96
98
  # @return [Hash]
97
- def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true)
99
+ def returns_spec_from_node(node, fallback_type: FALLBACK_TYPE, nil_as_optional: true, core_rbs_provider: nil,
100
+ param_types: nil)
98
101
  Returns.returns_spec_from_node(
99
102
  node,
100
103
  fallback_type: fallback_type,
101
- nil_as_optional: nil_as_optional
104
+ nil_as_optional: nil_as_optional,
105
+ core_rbs_provider: core_rbs_provider,
106
+ param_types: param_types
102
107
  )
103
108
  end
104
109