docscribe 1.0.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +692 -180
  3. data/exe/docscribe +2 -74
  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 +110 -259
  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 +604 -425
  33. data/lib/docscribe/parsing.rb +120 -0
  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. data/lib/docscribe.rb +1 -0
  43. metadata +85 -17
  44. data/.rspec +0 -3
  45. data/.rubocop.yml +0 -11
  46. data/.rubocop_todo.yml +0 -73
  47. data/CODE_OF_CONDUCT.md +0 -84
  48. data/Gemfile +0 -6
  49. data/Gemfile.lock +0 -73
  50. data/Rakefile +0 -12
  51. data/rakelib/docs.rake +0 -73
  52. data/stingray_docs_internal.gemspec +0 -41
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/source/buffer'
4
+ require 'rubygems' # for Gem::Version
5
+
6
+ module Docscribe
7
+ # Parser backend selection for Docscribe.
8
+ #
9
+ # Docscribe always works with parser-gem-compatible AST nodes (`Parser::AST::Node`)
10
+ # and parser source locations (`Parser::Source::*`) because rewriting relies on
11
+ # `Parser::Source::TreeRewriter`.
12
+ #
13
+ # On Ruby 3.4+, Prism can parse newer syntax before the parser gem fully supports it,
14
+ # so Docscribe can use Prism and translate the result into parser-gem-compatible nodes.
15
+ #
16
+ # Backends:
17
+ # - `:parser` => parser gem
18
+ # - `:prism` => Prism + translation
19
+ # - `:auto` => choose based on runtime Ruby version or env override
20
+ #
21
+ # You can force a backend with:
22
+ # - `DOCSCRIBE_PARSER_BACKEND=parser`
23
+ # - `DOCSCRIBE_PARSER_BACKEND=prism`
24
+ module Parsing
25
+ class << self
26
+ # Parse source code into a parser-gem-compatible AST.
27
+ #
28
+ # @param [String] code Ruby source
29
+ # @param [String] file source name used for parser locations
30
+ # @param [Symbol] backend :auto, :parser, or :prism
31
+ # @return [Parser::AST::Node, nil]
32
+ def parse(code, file: '(docscribe)', backend: :auto)
33
+ buffer = Parser::Source::Buffer.new(file, source: code)
34
+ parse_buffer(buffer, backend: backend)
35
+ end
36
+
37
+ # Parse a prepared source buffer into a parser-gem-compatible AST.
38
+ #
39
+ # @param [Parser::Source::Buffer] buffer
40
+ # @param [Symbol] backend :auto, :parser, or :prism
41
+ # @return [Parser::AST::Node, nil]
42
+ def parse_buffer(buffer, backend: :auto)
43
+ parser = parser_for(backend: backend)
44
+ parser.parse(buffer)
45
+ end
46
+
47
+ # Parse source code and also return comments when supported by the backend.
48
+ #
49
+ # @param [String] code Ruby source
50
+ # @param [String] file source name used for parser locations
51
+ # @param [Symbol] backend :auto, :parser, or :prism
52
+ # @return [Array<(Parser::AST::Node, Array)>]
53
+ def parse_with_comments(code, file: '(docscribe)', backend: :auto)
54
+ buffer = Parser::Source::Buffer.new(file, source: code)
55
+ parse_with_comments_buffer(buffer, backend: backend)
56
+ end
57
+
58
+ # Parse a prepared source buffer and also return comments when supported by the backend.
59
+ #
60
+ # @param [Parser::Source::Buffer] buffer
61
+ # @param [Symbol] backend :auto, :parser, or :prism
62
+ # @return [Array<(Parser::AST::Node, Array)>]
63
+ def parse_with_comments_buffer(buffer, backend: :auto)
64
+ parser = parser_for(backend: backend)
65
+ parser.parse_with_comments(buffer)
66
+ end
67
+
68
+ private
69
+
70
+ # Build the backend-specific parser object.
71
+ #
72
+ # @private
73
+ # @param [Symbol] backend
74
+ # @return [Object]
75
+ def parser_for(backend: :auto)
76
+ case backend(backend)
77
+ when :parser
78
+ require 'parser/current'
79
+ Parser::CurrentRuby.new
80
+ when :prism
81
+ require 'prism'
82
+ Prism::Translation::ParserCurrent.new
83
+ end
84
+ end
85
+
86
+ # Resolve the effective parser backend.
87
+ #
88
+ # Resolution order:
89
+ # - `DOCSCRIBE_PARSER_BACKEND` env var, if set
90
+ # - explicit `backend:` argument
91
+ # - auto choice based on Ruby version
92
+ #
93
+ # @private
94
+ # @param [Symbol] backend requested backend
95
+ # @raise [ArgumentError]
96
+ # @return [Symbol] :parser or :prism
97
+ def backend(backend = :auto)
98
+ env = ENV.fetch('DOCSCRIBE_PARSER_BACKEND') { nil }
99
+ backend = env.to_sym if env && !env.empty?
100
+
101
+ case backend
102
+ when :auto
103
+ ruby_gte_34? ? :prism : :parser
104
+ when :parser, :prism
105
+ backend
106
+ else
107
+ raise ArgumentError, "Unknown backend: #{backend.inspect}"
108
+ end
109
+ end
110
+
111
+ # Whether the current Ruby version is 3.4 or newer.
112
+ #
113
+ # @private
114
+ # @return [Boolean]
115
+ def ruby_gte_34?
116
+ Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.4')
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Types
5
+ # Resolve method signatures by querying a list of providers in order.
6
+ #
7
+ # The first provider that returns a non-nil signature wins.
8
+ #
9
+ # This lets Docscribe combine multiple external type sources behind one
10
+ # interface, for example:
11
+ # - inline Sorbet signatures in the current file
12
+ # - Sorbet RBI files
13
+ # - RBS files
14
+ class ProviderChain
15
+ # @param [Array<#signature_for>] providers ordered signature providers
16
+ # @return [Object]
17
+ def initialize(*providers)
18
+ @providers = providers.compact
19
+ end
20
+
21
+ # Resolve a method signature from the first provider that can supply it.
22
+ #
23
+ # @param [String] container e.g. "MyModule::MyClass"
24
+ # @param [Symbol] scope :instance or :class
25
+ # @param [Symbol, String] name method name
26
+ # @return [Docscribe::Types::MethodSignature, nil]
27
+ def signature_for(container:, scope:, name:)
28
+ @providers.each do |provider|
29
+ sig = provider.signature_for(container: container, scope: scope, name: name)
30
+ return sig if sig
31
+ end
32
+
33
+ nil
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'docscribe/types/signature'
5
+ require 'docscribe/types/rbs/type_formatter'
6
+
7
+ module Docscribe
8
+ module Types
9
+ module RBS
10
+ # Resolve method signatures from `.rbs` files using the official RBS
11
+ # environment and definition builder APIs.
12
+ #
13
+ # The provider returns Docscribe's normalized signature model so the rest of
14
+ # the pipeline can stay independent of the underlying signature source.
15
+ class Provider
16
+ # @param [Array<String>] sig_dirs directories containing `.rbs` files
17
+ # @param [Boolean] collapse_generics whether generic container types
18
+ # should be simplified during formatting
19
+ # @return [Object]
20
+ def initialize(sig_dirs:, collapse_generics: false)
21
+ require 'rbs'
22
+ @sig_dirs = Array(sig_dirs).map(&:to_s)
23
+ @collapse_generics = !!collapse_generics
24
+ @env = nil
25
+ @builder = nil
26
+ @warned = false
27
+ end
28
+
29
+ # Look up a normalized method signature from loaded RBS definitions.
30
+ #
31
+ # Returns nil when the method cannot be resolved or when RBS lookup fails.
32
+ #
33
+ # @param [String] container e.g. "MyModule::MyClass"
34
+ # @param [Symbol] scope :instance or :class
35
+ # @param [Symbol, String] name method name
36
+ # @raise [::RBS::BaseError]
37
+ # @raise [StandardError]
38
+ # @return [Docscribe::Types::MethodSignature, nil]
39
+ def signature_for(container:, scope:, name:)
40
+ load_env!
41
+
42
+ definition = definition_for(container: container, scope: scope)
43
+ method_def = definition.methods[name.to_sym]
44
+ return nil unless method_def
45
+
46
+ method_type = method_def.method_types.first
47
+ return nil unless method_type
48
+
49
+ func = method_type.type
50
+ build_signature(func)
51
+ rescue ::RBS::BaseError => e
52
+ warn_once("Docscribe: RBS error: #{e.class}: #{e.message}")
53
+ nil
54
+ rescue StandardError => e
55
+ warn_once(
56
+ 'Docscribe: RBS integration failed (falling back to inference): ' \
57
+ "#{e.class}: #{e.message}\nFeel free to open an issue on github."
58
+ )
59
+ nil
60
+ end
61
+
62
+ private
63
+
64
+ # Lazily load and resolve the RBS environment.
65
+ #
66
+ # @private
67
+ # @return [void]
68
+ def load_env!
69
+ return if @env && @builder
70
+
71
+ loader = ::RBS::EnvironmentLoader.new
72
+
73
+ @sig_dirs.each do |dir|
74
+ path = Pathname(dir)
75
+ loader.add(path: path) if path.directory?
76
+ end
77
+
78
+ @env = ::RBS::Environment.from_loader(loader).resolve_type_names
79
+ @builder = ::RBS::DefinitionBuilder.new(env: @env)
80
+ end
81
+
82
+ # Build the appropriate instance or singleton definition for a container.
83
+ #
84
+ # @private
85
+ # @param [String] container
86
+ # @param [Symbol] scope
87
+ # @return [Object]
88
+ def definition_for(container:, scope:)
89
+ type_name = ::RBS::TypeName.parse(absolute_const(container))
90
+ scope == :class ? @builder.build_singleton(type_name) : @builder.build_instance(type_name)
91
+ end
92
+
93
+ # Normalize a container name into an absolute constant path.
94
+ #
95
+ # @private
96
+ # @param [String] container
97
+ # @return [String]
98
+ def absolute_const(container)
99
+ s = container.to_s
100
+ s.start_with?('::') ? s : "::#{s}"
101
+ end
102
+
103
+ # Convert an RBS function type into Docscribe's simplified signature
104
+ # model.
105
+ #
106
+ # @private
107
+ # @param [::RBS::Types::Function] func
108
+ # @return [Docscribe::Types::MethodSignature]
109
+ def build_signature(func)
110
+ MethodSignature.new(
111
+ return_type: format_type(func.return_type),
112
+ param_types: build_param_types(func),
113
+ rest_positional: build_rest_positional(func),
114
+ rest_keywords: build_rest_keywords(func)
115
+ )
116
+ end
117
+
118
+ # Build a name => type map for positional and keyword parameters.
119
+ #
120
+ # @private
121
+ # @param [::RBS::Types::Function] func
122
+ # @return [Hash{String => String}]
123
+ def build_param_types(func)
124
+ param_types = {}
125
+
126
+ add_positionals!(param_types, func.required_positionals)
127
+ add_positionals!(param_types, func.optional_positionals)
128
+ add_positionals!(param_types, func.trailing_positionals)
129
+
130
+ func.required_keywords.each do |kw, p|
131
+ param_types[kw.to_s] = format_type(p.type)
132
+ end
133
+
134
+ func.optional_keywords.each do |kw, p|
135
+ param_types[kw.to_s] = format_type(p.type)
136
+ end
137
+
138
+ param_types
139
+ end
140
+
141
+ # Add named positional parameters to the normalized parameter map.
142
+ #
143
+ # @private
144
+ # @param [Hash{String => String}] param_types
145
+ # @param [Array<Object>] list
146
+ # @return [void]
147
+ def add_positionals!(param_types, list)
148
+ list.each do |p|
149
+ next unless p.name
150
+
151
+ param_types[p.name.to_s] = format_type(p.type)
152
+ end
153
+ end
154
+
155
+ # Build normalized `*args` metadata.
156
+ #
157
+ # @private
158
+ # @param [::RBS::Types::Function] func
159
+ # @return [Docscribe::Types::RestPositional, nil]
160
+ def build_rest_positional(func)
161
+ rp = func.rest_positionals
162
+ return nil unless rp
163
+
164
+ RestPositional.new(
165
+ name: rp.name&.to_s,
166
+ element_type: format_type(rp.type)
167
+ )
168
+ end
169
+
170
+ # Build normalized `**kwargs` metadata.
171
+ #
172
+ # @private
173
+ # @param [::RBS::Types::Function] func
174
+ # @return [Docscribe::Types::RestKeywords, nil]
175
+ def build_rest_keywords(func)
176
+ rk = func.rest_keywords
177
+ return nil unless rk
178
+
179
+ RestKeywords.new(
180
+ name: rk.name&.to_s,
181
+ type: format_type(rk.type)
182
+ )
183
+ end
184
+
185
+ # Format an RBS type object into the YARD-ish type syntax used by
186
+ # generated comments.
187
+ #
188
+ # @private
189
+ # @param [Object] type
190
+ # @return [String]
191
+ def format_type(type)
192
+ Docscribe::Types::RBS::TypeFormatter.to_yard(
193
+ type,
194
+ collapse_generics: @collapse_generics
195
+ )
196
+ end
197
+
198
+ # Print one debug warning per provider instance when debugging is enabled.
199
+ #
200
+ # @private
201
+ # @param [String] msg
202
+ # @return [void]
203
+ def warn_once(msg)
204
+ return unless ENV['DOCSCRIBE_RBS_DEBUG'] == '1'
205
+ return if @warned
206
+
207
+ @warned = true
208
+ warn msg
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Types
5
+ module RBS
6
+ # Convert RBS type objects into YARD-ish type strings.
7
+ #
8
+ # This is intentionally best-effort formatting: YARD type syntax is simpler than RBS,
9
+ # so some information is collapsed or approximated.
10
+ module TypeFormatter
11
+ module_function
12
+
13
+ # Convert one RBS type object into a YARD-ish string.
14
+ #
15
+ # Supported categories include:
16
+ # - base types (`bool`, `nil`, `void`, `untyped`)
17
+ # - optional and union types
18
+ # - named types with optional generic arguments
19
+ # - literal types
20
+ # - Proc types
21
+ #
22
+ # @note module_function: when included, also defines #to_yard (instance visibility: private)
23
+ # @param [Object] type RBS type object
24
+ # @param [Boolean] collapse_generics whether generic arguments should be omitted
25
+ # @return [String]
26
+ def to_yard(type, collapse_generics: false)
27
+ return 'Object' unless type
28
+
29
+ # RBS is loaded lazily by the provider; constants below exist only when rbs is available.
30
+ case type
31
+ when ::RBS::Types::Bases::Any
32
+ 'Object'
33
+ when ::RBS::Types::Bases::Bool
34
+ 'Boolean'
35
+ when ::RBS::Types::Bases::Void
36
+ 'void'
37
+ when ::RBS::Types::Bases::Nil
38
+ 'nil'
39
+ when ::RBS::Types::Optional
40
+ "#{to_yard(type.type, collapse_generics: collapse_generics)}?"
41
+ when ::RBS::Types::Union
42
+ format_union(type, collapse_generics: collapse_generics)
43
+ when ::RBS::Types::ClassInstance,
44
+ ::RBS::Types::ClassSingleton,
45
+ ::RBS::Types::Interface,
46
+ ::RBS::Types::Alias
47
+ format_named(type, collapse_generics: collapse_generics)
48
+ when ::RBS::Types::Literal
49
+ literal_to_yard(type.literal)
50
+ when ::RBS::Types::Proc
51
+ 'Proc'
52
+ else
53
+ fallback_string(type)
54
+ end
55
+ end
56
+
57
+ # Format an RBS union type as a comma-separated YARD union.
58
+ #
59
+ # Example:
60
+ # - `String | Integer | nil` => `"String, Integer, nil"`
61
+ #
62
+ # @note module_function: when included, also defines #format_union (instance visibility: private)
63
+ # @param [::RBS::Types::Union] type
64
+ # @param [Boolean] collapse_generics
65
+ # @return [String]
66
+ def format_union(type, collapse_generics:)
67
+ type.types.map { |t| to_yard(t, collapse_generics: collapse_generics) }
68
+ .uniq
69
+ .join(', ')
70
+ end
71
+
72
+ # Format a named RBS type, optionally preserving generic arguments.
73
+ #
74
+ # Examples:
75
+ # - `::String` => `"String"`
76
+ # - `::Hash[::Symbol, untyped]` => `"Hash<Symbol, Object>"`
77
+ # - with `collapse_generics: true` => `"Hash"`
78
+ #
79
+ # @note module_function: when included, also defines #format_named (instance visibility: private)
80
+ # @param [Object] type named RBS type
81
+ # @param [Boolean] collapse_generics
82
+ # @return [String]
83
+ def format_named(type, collapse_generics:)
84
+ name = type.name.to_s.delete_prefix('::')
85
+ args = type.respond_to?(:args) ? type.args : []
86
+
87
+ if args && !args.empty?
88
+ return name if collapse_generics
89
+
90
+ "#{name}<#{args.map { |a| to_yard(a, collapse_generics: collapse_generics) }.join(', ')}>"
91
+ else
92
+ name
93
+ end
94
+ end
95
+
96
+ # Map a literal Ruby value from an RBS literal type into a YARD-ish type name.
97
+ #
98
+ # @note module_function: when included, also defines #literal_to_yard (instance visibility: private)
99
+ # @param [Object] lit literal value
100
+ # @return [String]
101
+ def literal_to_yard(lit)
102
+ case lit
103
+ when Integer then 'Integer'
104
+ when Float then 'Float'
105
+ when String then 'String'
106
+ when Symbol then 'Symbol'
107
+ when TrueClass, FalseClass then 'Boolean'
108
+ when NilClass then 'nil'
109
+ else 'Object'
110
+ end
111
+ end
112
+
113
+ # Fallback string conversion for unsupported or unexpected RBS type objects.
114
+ #
115
+ # Performs a few normalizations for nicer YARD output:
116
+ # - strips leading `::`
117
+ # - converts `bool` to `Boolean`
118
+ # - converts `untyped` to `Object`
119
+ #
120
+ # @note module_function: when included, also defines #fallback_string (instance visibility: private)
121
+ # @param [Object] type
122
+ # @return [String]
123
+ def fallback_string(type)
124
+ type.to_s
125
+ .gsub(/\A::/, '')
126
+ .gsub(/\bbool\b/, 'Boolean')
127
+ .gsub(/\buntyped\b/, 'Object')
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Types
5
+ # Simplified view of an RBS method signature for Docscribe.
6
+ #
7
+ # @!attribute return_type
8
+ # @return [String] formatted return type for YARD output
9
+ # @!attribute param_types
10
+ # @return [Hash{String=>String}] mapping of parameter name to formatted type
11
+ # @!attribute rest_positional
12
+ # @return [RestPositional, nil] info for `*args`
13
+ # @!attribute rest_keywords
14
+ # @return [RestKeywords, nil] info for `**kwargs`
15
+ #
16
+ # @!attribute [rw] return_type
17
+ # @return [Object]
18
+ # @param [Object] value
19
+ #
20
+ # @!attribute [rw] param_types
21
+ # @return [Object]
22
+ # @param [Object] value
23
+ #
24
+ # @!attribute [rw] rest_positional
25
+ # @return [Object]
26
+ # @param [Object] value
27
+ #
28
+ # @!attribute [rw] rest_keywords
29
+ # @return [Object]
30
+ # @param [Object] value
31
+ MethodSignature = Struct.new(:return_type, :param_types, :rest_positional, :rest_keywords, keyword_init: true)
32
+
33
+ # Simplified representation of an RBS rest-positional parameter.
34
+ #
35
+ # @!attribute name
36
+ # @return [String, nil] parameter name in RBS, if present
37
+ # @!attribute element_type
38
+ # @return [String] formatted element type
39
+ #
40
+ # @!attribute [rw] name
41
+ # @return [Object]
42
+ # @param [Object] value
43
+ #
44
+ # @!attribute [rw] element_type
45
+ # @return [Object]
46
+ # @param [Object] value
47
+ RestPositional = Struct.new(:name, :element_type, keyword_init: true)
48
+
49
+ # Simplified representation of an RBS rest-keyword parameter.
50
+ #
51
+ # @!attribute name
52
+ # @return [String, nil] parameter name in RBS, if present
53
+ # @!attribute type
54
+ # @return [String] formatted kwargs type
55
+ #
56
+ # @!attribute [rw] name
57
+ # @return [Object]
58
+ # @param [Object] value
59
+ #
60
+ # @!attribute [rw] type
61
+ # @return [Object]
62
+ # @param [Object] value
63
+ RestKeywords = Struct.new(:name, :type, keyword_init: true)
64
+ end
65
+ end