docscribe 1.1.0 → 1.2.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.
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 +126 -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 +176 -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 +605 -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
@@ -1,245 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
+ require 'pathname'
5
+ require 'psych'
4
6
 
5
7
  module Docscribe
6
8
  class Config
7
- DEFAULT = {
8
- 'emit' => {
9
- 'header' => true,
10
- 'param_tags' => true,
11
- 'return_tag' => true,
12
- 'visibility_tags' => true,
13
- 'raise_tags' => true,
14
- 'rescue_conditional_returns' => true
15
- },
16
- 'doc' => {
17
- 'default_message' => 'Method documentation.'
18
- },
19
- 'methods' => {
20
- 'instance' => {
21
- 'public' => {},
22
- 'protected' => {},
23
- 'private' => {}
24
- },
25
- 'class' => {
26
- 'public' => {},
27
- 'protected' => {},
28
- 'private' => {}
29
- }
30
- },
31
- 'inference' => {
32
- 'fallback_type' => 'Object',
33
- 'nil_as_optional' => true,
34
- 'treat_options_keyword_as_hash' => true
35
- },
36
- 'filter' => {
37
- 'visibilities' => %w[public protected private],
38
- 'scopes' => %w[instance class],
39
- 'include' => [],
40
- 'exclude' => []
41
- }
42
- }.freeze
43
-
9
+ # Raw config hash after deep-merging user config with defaults.
10
+ #
11
+ # @!attribute [r] raw
12
+ # @return [Hash]
44
13
  attr_reader :raw
45
14
 
46
- # +Docscribe::Config#initialize+ -> Object
15
+ # Create a configuration object from a raw config hash.
47
16
  #
48
- # Method documentation.
17
+ # Missing keys are filled from {DEFAULT} via deep merge.
49
18
  #
50
- # @param [Hash] raw Param documentation.
51
- # @return [Object]
19
+ # @param [Hash, nil] raw user-provided config hash
20
+ # @return [void]
52
21
  def initialize(raw = {})
53
22
  @raw = deep_merge(DEFAULT, raw || {})
54
23
  end
55
-
56
- # +Docscribe::Config.load+ -> Object
57
- #
58
- # Method documentation.
59
- #
60
- # @param [nil] path Param documentation.
61
- # @return [Object]
62
- def self.load(path = nil)
63
- raw = {}
64
- if path && File.file?(path)
65
- raw = YAML.safe_load_file(path, permitted_classes: [], aliases: true) || {}
66
- elsif File.file?('docscribe.yml')
67
- raw = YAML.safe_load_file('docscribe.yml', permitted_classes: [], aliases: true) || {}
68
- end
69
- new(raw)
70
- end
71
-
72
- # +Docscribe::Config#emit_header?+ -> Object
73
- #
74
- # Method documentation.
75
- #
76
- # @return [Object]
77
- def emit_header?
78
- fetch_bool(%w[emit header], true)
79
- end
80
-
81
- # +Docscribe::Config#emit_param_tags?+ -> Object
82
- #
83
- # Method documentation.
84
- #
85
- # @return [Object]
86
- def emit_param_tags?
87
- fetch_bool(%w[emit param_tags], true)
88
- end
89
-
90
- # +Docscribe::Config#emit_visibility_tags?+ -> Object
91
- #
92
- # Method documentation.
93
- #
94
- # @return [Object]
95
- def emit_visibility_tags?
96
- fetch_bool(%w[emit visibility_tags], true)
97
- end
98
-
99
- # +Docscribe::Config#emit_raise_tags?+ -> Object
100
- #
101
- # Method documentation.
102
- #
103
- # @return [Object]
104
- def emit_raise_tags?
105
- fetch_bool(%w[emit raise_tags], true)
106
- end
107
-
108
- # +Docscribe::Config#emit_rescue_conditional_returns?+ -> Object
109
- #
110
- # Method documentation.
111
- #
112
- # @return [Object]
113
- def emit_rescue_conditional_returns?
114
- fetch_bool(%w[emit rescue_conditional_returns], true)
115
- end
116
-
117
- # +Docscribe::Config#emit_return_tag?+ -> Object
118
- #
119
- # Method documentation.
120
- #
121
- # @param [Object] scope Param documentation.
122
- # @param [Object] visibility Param documentation.
123
- # @return [Object]
124
- def emit_return_tag?(scope, visibility)
125
- method_override_bool(scope, visibility, 'return_tag',
126
- default: fetch_bool(%w[emit return_tag], true))
127
- end
128
-
129
- # +Docscribe::Config#default_message+ -> Object
130
- #
131
- # Method documentation.
132
- #
133
- # @param [Object] scope Param documentation.
134
- # @param [Object] visibility Param documentation.
135
- # @return [Object]
136
- def default_message(scope, visibility)
137
- method_override_str(scope, visibility, 'default_message',
138
- default: raw.dig('doc', 'default_message') || 'Method documentation.')
139
- end
140
-
141
- # +Docscribe::Config#fallback_type+ -> Object
142
- #
143
- # Method documentation.
144
- #
145
- # @return [Object]
146
- def fallback_type
147
- raw.dig('inference', 'fallback_type') || 'Object'
148
- end
149
-
150
- # +Docscribe::Config#nil_as_optional?+ -> Object
151
- #
152
- # Method documentation.
153
- #
154
- # @return [Object]
155
- def nil_as_optional?
156
- fetch_bool(%w[inference nil_as_optional], true)
157
- end
158
-
159
- # +Docscribe::Config#treat_options_keyword_as_hash?+ -> Object
160
- #
161
- # Method documentation.
162
- #
163
- # @return [Object]
164
- def treat_options_keyword_as_hash?
165
- fetch_bool(%w[inference treat_options_keyword_as_hash], true)
166
- end
167
-
168
- private
169
-
170
- # +Docscribe::Config#method_override_bool+ -> Object
171
- #
172
- # Method documentation.
173
- #
174
- # @private
175
- # @param [Object] scope Param documentation.
176
- # @param [Object] vis Param documentation.
177
- # @param [Object] key Param documentation.
178
- # @param [Object] default Param documentation.
179
- # @return [Object]
180
- def method_override_bool(scope, vis, key, default:)
181
- node = raw.dig('methods', scope_to_key(scope), vis.to_s, key)
182
- node.nil? ? default : !!node
183
- end
184
-
185
- # +Docscribe::Config#fetch_bool+ -> Object
186
- #
187
- # Method documentation.
188
- #
189
- # @private
190
- # @param [Object] path Param documentation.
191
- # @param [Object] default Param documentation.
192
- # @return [Object]
193
- def fetch_bool(path, default)
194
- node = raw
195
- path.each { |k| node = node[k] if node }
196
- node.nil? ? default : !!node
197
- end
198
-
199
- # +Docscribe::Config#method_override_str+ -> Object
200
- #
201
- # Method documentation.
202
- #
203
- # @private
204
- # @param [Object] scope Param documentation.
205
- # @param [Object] vis Param documentation.
206
- # @param [Object] key Param documentation.
207
- # @param [Object] default Param documentation.
208
- # @return [Object]
209
- def method_override_str(scope, vis, key, default:)
210
- node = raw.dig('methods', scope_to_key(scope), vis.to_s, key)
211
- node.nil? ? default : node.to_s
212
- end
213
-
214
- # +Docscribe::Config#scope_to_key+ -> String
215
- #
216
- # Method documentation.
217
- #
218
- # @private
219
- # @param [Object] scope Param documentation.
220
- # @return [String]
221
- def scope_to_key(scope)
222
- scope == :class ? 'class' : 'instance'
223
- end
224
-
225
- # +Docscribe::Config#deep_merge+ -> Object
226
- #
227
- # Method documentation.
228
- #
229
- # @private
230
- # @param [Object] hash1 Param documentation.
231
- # @param [Object] hash2 Param documentation.
232
- # @return [Object]
233
- def deep_merge(hash1, hash2)
234
- return hash1 unless hash2
235
-
236
- hash1.merge(hash2) do |_, v1, v2|
237
- if v1.is_a?(Hash) && v2.is_a?(Hash)
238
- deep_merge(v1, v2)
239
- else
240
- v2
241
- end
242
- end
243
- end
244
24
  end
245
25
  end
26
+
27
+ require_relative 'config/defaults'
28
+ require_relative 'config/utils'
29
+ require_relative 'config/loader'
30
+ require_relative 'config/template'
31
+ require_relative 'config/emit'
32
+ require_relative 'config/filtering'
33
+ require_relative 'config/rbs'
34
+ require_relative 'config/sorting'
35
+ require_relative 'config/sorbet'
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Infer
5
+ # AST traversal helpers for parser AST nodes.
6
+ module ASTWalk
7
+ module_function
8
+
9
+ # Depth-first walk over a parser AST.
10
+ #
11
+ # Yields each node exactly once, descending recursively through child nodes.
12
+ # Non-AST values are ignored.
13
+ #
14
+ # @note module_function: when included, also defines #walk (instance visibility: private)
15
+ # @param [Parser::AST::Node, nil] node root AST node
16
+ # @param [Proc] block visitor block
17
+ # @return [void]
18
+ def walk(node, &block)
19
+ return unless node.is_a?(Parser::AST::Node)
20
+
21
+ yield node
22
+ node.children.each do |ch|
23
+ walk(ch, &block) if ch.is_a?(Parser::AST::Node)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Infer
5
+ # Default fallback type used when inference cannot be certain.
6
+ FALLBACK_TYPE = 'Object'
7
+
8
+ # Ruby's implicit rescue target for bare `rescue`.
9
+ DEFAULT_ERROR = 'StandardError'
10
+ end
11
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Infer
5
+ # Literal inference: map simple AST literals to type names.
6
+ module Literals
7
+ module_function
8
+
9
+ # Infer a type name from a literal-like AST node.
10
+ #
11
+ # Supports common literal/value node types such as:
12
+ # - integers, floats, strings, symbols
13
+ # - booleans and nil
14
+ # - arrays, hashes, regexps
15
+ # - constants
16
+ # - `Foo.new` constructor calls
17
+ #
18
+ # If the node does not match a supported pattern, the fallback type is returned.
19
+ #
20
+ # @note module_function: when included, also defines #type_from_literal (instance visibility: private)
21
+ # @param [Parser::AST::Node, nil] node literal/value node
22
+ # @param [String] fallback_type type returned when inference is uncertain
23
+ # @return [String]
24
+ def type_from_literal(node, fallback_type: FALLBACK_TYPE)
25
+ return fallback_type unless node
26
+
27
+ case node.type
28
+ when :int then 'Integer'
29
+ when :float then 'Float'
30
+ when :str, :dstr then 'String'
31
+ when :sym then 'Symbol'
32
+ when :true, :false then 'Boolean' # rubocop:disable Lint/BooleanSymbol
33
+ when :nil then 'nil'
34
+ when :array then 'Array'
35
+ when :hash then 'Hash'
36
+ when :regexp then 'Regexp'
37
+
38
+ when :const
39
+ node.children.last.to_s
40
+
41
+ when :send
42
+ recv, meth, = node.children
43
+ if meth == :new && recv && recv.type == :const
44
+ recv.children.last.to_s
45
+ else
46
+ fallback_type
47
+ end
48
+
49
+ else
50
+ fallback_type
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Infer
5
+ # Constant-name helpers for turning AST nodes into fully qualified names.
6
+ module Names
7
+ module_function
8
+
9
+ # Convert a `:const` / `:cbase` AST node into a fully qualified constant name.
10
+ #
11
+ # Examples:
12
+ # - `Foo` => `"Foo"`
13
+ # - `Foo::Bar` => `"Foo::Bar"`
14
+ # - `::Foo::Bar` => `"::Foo::Bar"`
15
+ #
16
+ # Returns nil for unsupported nodes.
17
+ #
18
+ # @note module_function: when included, also defines #const_full_name (instance visibility: private)
19
+ # @param [Parser::AST::Node, nil] n constant-like AST node
20
+ # @return [String, nil]
21
+ def const_full_name(n)
22
+ return nil unless n.is_a?(Parser::AST::Node)
23
+
24
+ case n.type
25
+ when :const
26
+ scope, name = *n
27
+ scope_name = const_full_name(scope)
28
+
29
+ if scope_name && !scope_name.empty?
30
+ "#{scope_name}::#{name}"
31
+ elsif scope_name == '' # leading ::
32
+ "::#{name}"
33
+ else
34
+ name.to_s
35
+ end
36
+
37
+ when :cbase
38
+ '' # represents leading :: scope
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Infer
5
+ # Parameter type inference.
6
+ module Params
7
+ module_function
8
+
9
+ # Infer a parameter type from an internal parameter name representation and
10
+ # an optional default expression.
11
+ #
12
+ # Handles:
13
+ # - positional/rest/block parameter prefixes (`*`, `**`, `&`)
14
+ # - keyword params with and without defaults
15
+ # - special-casing `options:` as `Hash` when enabled
16
+ # - literal defaults via AST parsing
17
+ #
18
+ # @note module_function: when included, also defines #infer_param_type (instance visibility: private)
19
+ # @param [String] name parameter name as used internally (may include `*`, `**`, `&`, or trailing `:`)
20
+ # @param [String, nil] default_str source for the default value expression
21
+ # @param [String] fallback_type type returned when inference is uncertain
22
+ # @param [Boolean] treat_options_keyword_as_hash whether `options:` should
23
+ # be treated specially as Hash
24
+ # @return [String]
25
+ def infer_param_type(name, default_str, fallback_type: FALLBACK_TYPE, treat_options_keyword_as_hash: true)
26
+ return 'Array' if name.start_with?('*') && !name.start_with?('**')
27
+ return 'Hash' if name.start_with?('**')
28
+ return 'Proc' if name.start_with?('&')
29
+
30
+ is_kw = name.end_with?(':')
31
+ node = parse_expr(default_str)
32
+ ty = Literals.type_from_literal(node, fallback_type: fallback_type)
33
+
34
+ if is_kw && default_str.nil?
35
+ return (treat_options_keyword_as_hash && name == 'options:' ? 'Hash' : fallback_type)
36
+ end
37
+
38
+ return 'Hash' if treat_options_keyword_as_hash && name == 'options:' && (default_str == '{}' || ty == 'Hash')
39
+
40
+ ty
41
+ end
42
+
43
+ # Parse a standalone expression for parameter-default inference.
44
+ #
45
+ # Returns nil if the expression is empty or cannot be parsed.
46
+ #
47
+ # @note module_function: when included, also defines #parse_expr (instance visibility: private)
48
+ # @param [String, nil] src expression source
49
+ # @raise [Parser::SyntaxError]
50
+ # @return [Parser::AST::Node, nil]
51
+ def parse_expr(src)
52
+ return nil if src.nil? || src.strip.empty?
53
+
54
+ buffer = Parser::Source::Buffer.new('(param)')
55
+ buffer.source = src
56
+ Docscribe::Parsing.parse_buffer(buffer)
57
+ rescue Parser::SyntaxError
58
+ nil
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Infer
5
+ # Exception inference from AST (`raise`/`fail` calls and `rescue` clauses).
6
+ module Raises
7
+ module_function
8
+
9
+ # Infer exception class names raised or rescued within a node.
10
+ #
11
+ # Sources considered:
12
+ # - `rescue Foo, Bar`
13
+ # - bare `rescue` (=> StandardError)
14
+ # - `raise Foo`
15
+ # - bare `raise` / `fail` (=> StandardError)
16
+ #
17
+ # Returns unique exception names in discovery order.
18
+ #
19
+ # @note module_function: when included, also defines #infer_raises_from_node (instance visibility: private)
20
+ # @param [Parser::AST::Node] node method or expression node to inspect
21
+ # @return [Array<String>]
22
+ def infer_raises_from_node(node)
23
+ raises = []
24
+
25
+ ASTWalk.walk(node) do |n|
26
+ case n.type
27
+ when :resbody
28
+ exc_list = n.children[0]
29
+ raises.concat(exception_names_from_rescue_list(exc_list))
30
+
31
+ when :send
32
+ recv, meth, *args = *n
33
+ next unless recv.nil? && %i[raise fail].include?(meth)
34
+
35
+ if args.empty?
36
+ raises << DEFAULT_ERROR
37
+ else
38
+ c = Names.const_full_name(args[0])
39
+ raises << (c || DEFAULT_ERROR)
40
+ end
41
+ end
42
+ end
43
+
44
+ raises.uniq
45
+ end
46
+
47
+ # Extract exception class names from a rescue exception list.
48
+ #
49
+ # Examples:
50
+ # - nil => `[StandardError]`
51
+ # - `Foo` => `["Foo"]`
52
+ # - `[Foo, Bar]` => `["Foo", "Bar"]`
53
+ #
54
+ # @note module_function: when included, also defines #exception_names_from_rescue_list (instance visibility: private)
55
+ # @param [Parser::AST::Node, nil] exc_list rescue exception list node
56
+ # @return [Array<String>]
57
+ def exception_names_from_rescue_list(exc_list)
58
+ if exc_list.nil?
59
+ [DEFAULT_ERROR]
60
+ elsif exc_list.type == :array
61
+ exc_list.children.map { |e| Names.const_full_name(e) || DEFAULT_ERROR }
62
+ else
63
+ [Names.const_full_name(exc_list) || DEFAULT_ERROR]
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end