docscribe 1.4.1 → 1.5.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +588 -104
  3. data/lib/docscribe/cli/check_for_comments.rb +183 -0
  4. data/lib/docscribe/cli/config_builder.rb +180 -36
  5. data/lib/docscribe/cli/formatters/json.rb +294 -0
  6. data/lib/docscribe/cli/formatters/sarif.rb +235 -0
  7. data/lib/docscribe/cli/formatters/text.rb +208 -0
  8. data/lib/docscribe/cli/formatters.rb +26 -0
  9. data/lib/docscribe/cli/generate.rb +296 -125
  10. data/lib/docscribe/cli/init.rb +58 -14
  11. data/lib/docscribe/cli/options.rb +410 -133
  12. data/lib/docscribe/cli/rbs_gen.rb +529 -0
  13. data/lib/docscribe/cli/run.rb +503 -189
  14. data/lib/docscribe/cli/sigs.rb +366 -0
  15. data/lib/docscribe/cli/update_types.rb +103 -0
  16. data/lib/docscribe/cli.rb +35 -9
  17. data/lib/docscribe/config/defaults.rb +16 -12
  18. data/lib/docscribe/config/emit.rb +18 -0
  19. data/lib/docscribe/config/filtering.rb +37 -31
  20. data/lib/docscribe/config/loader.rb +20 -13
  21. data/lib/docscribe/config/plugin.rb +2 -1
  22. data/lib/docscribe/config/rbs.rb +68 -27
  23. data/lib/docscribe/config/sorbet.rb +40 -17
  24. data/lib/docscribe/config/sorting.rb +2 -1
  25. data/lib/docscribe/config/template.rb +10 -1
  26. data/lib/docscribe/config/utils.rb +12 -9
  27. data/lib/docscribe/config.rb +3 -4
  28. data/lib/docscribe/infer/ast_walk.rb +1 -1
  29. data/lib/docscribe/infer/constants.rb +15 -0
  30. data/lib/docscribe/infer/literals.rb +39 -26
  31. data/lib/docscribe/infer/names.rb +24 -16
  32. data/lib/docscribe/infer/params.rb +57 -13
  33. data/lib/docscribe/infer/raises.rb +23 -15
  34. data/lib/docscribe/infer/returns.rb +784 -199
  35. data/lib/docscribe/infer.rb +28 -28
  36. data/lib/docscribe/inline_rewriter/collector.rb +816 -430
  37. data/lib/docscribe/inline_rewriter/doc_block.rb +323 -150
  38. data/lib/docscribe/inline_rewriter/doc_builder.rb +1837 -648
  39. data/lib/docscribe/inline_rewriter/source_helpers.rb +119 -71
  40. data/lib/docscribe/inline_rewriter/tag_sorter.rb +165 -107
  41. data/lib/docscribe/inline_rewriter.rb +1144 -727
  42. data/lib/docscribe/parsing.rb +29 -10
  43. data/lib/docscribe/plugin/base/collector_plugin.rb +3 -3
  44. data/lib/docscribe/plugin/base/tag_plugin.rb +1 -2
  45. data/lib/docscribe/plugin/context.rb +28 -18
  46. data/lib/docscribe/plugin/registry.rb +49 -23
  47. data/lib/docscribe/plugin/tag.rb +9 -14
  48. data/lib/docscribe/plugin.rb +54 -22
  49. data/lib/docscribe/types/provider_chain.rb +4 -2
  50. data/lib/docscribe/types/rbs/collection_loader.rb +2 -3
  51. data/lib/docscribe/types/rbs/provider.rb +127 -62
  52. data/lib/docscribe/types/rbs/type_formatter.rb +286 -77
  53. data/lib/docscribe/types/signature.rb +22 -42
  54. data/lib/docscribe/types/sorbet/base_provider.rb +51 -27
  55. data/lib/docscribe/types/sorbet/rbi_provider.rb +3 -3
  56. data/lib/docscribe/types/sorbet/source_provider.rb +3 -2
  57. data/lib/docscribe/types/yard/formatter.rb +100 -0
  58. data/lib/docscribe/types/yard/parser.rb +240 -0
  59. data/lib/docscribe/types/yard/types.rb +52 -0
  60. data/lib/docscribe/version.rb +1 -1
  61. metadata +34 -2
@@ -36,20 +36,31 @@ module Docscribe
36
36
 
37
37
  # Parse a prepared source buffer into a parser-gem-compatible AST.
38
38
  #
39
- # @param [Parser::Source::Buffer] buffer
39
+ # Returns +nil+ when the source cannot be parsed (syntax errors or
40
+ # internal parser crashes).
41
+ #
42
+ # @param [Parser::Source::Buffer] buffer prepared source buffer
40
43
  # @param [Symbol] backend :auto, :parser, or :prism
41
- # @return [Parser::AST::Node, nil]
44
+ # @raise [NoMethodError]
45
+ # @return [Parser::AST::Node, nil] if NoMethodError
46
+ # @return [nil] if NoMethodError
42
47
  def parse_buffer(buffer, backend: :auto)
43
48
  parser = parser_for(backend: backend)
49
+ return nil unless parser
50
+
44
51
  parser.parse(buffer)
52
+ rescue NoMethodError
53
+ nil
45
54
  end
46
55
 
47
56
  # Parse source code and also return comments when supported by the backend.
48
57
  #
58
+ # Returns +nil+ when the source cannot be parsed.
59
+ #
49
60
  # @param [String] code Ruby source
50
61
  # @param [String] file source name used for parser locations
51
62
  # @param [Symbol] backend :auto, :parser, or :prism
52
- # @return [Array<(Parser::AST::Node, Array)>]
63
+ # @return [(Parser::AST::Node?, Array<Parser::Source::Comment>), nil]
53
64
  def parse_with_comments(code, file: '(docscribe)', backend: :auto)
54
65
  buffer = Parser::Source::Buffer.new(file, source: code)
55
66
  parse_with_comments_buffer(buffer, backend: backend)
@@ -57,12 +68,20 @@ module Docscribe
57
68
 
58
69
  # Parse a prepared source buffer and also return comments when supported by the backend.
59
70
  #
60
- # @param [Parser::Source::Buffer] buffer
71
+ # Returns +nil+ when the source cannot be parsed.
72
+ #
73
+ # @param [Parser::Source::Buffer] buffer prepared source buffer
61
74
  # @param [Symbol] backend :auto, :parser, or :prism
62
- # @return [Array<(Parser::AST::Node, Array)>]
75
+ # @raise [NoMethodError]
76
+ # @return [(Parser::AST::Node?, Array<Parser::Source::Comment>), nil] if NoMethodError
77
+ # @return [nil] if NoMethodError
63
78
  def parse_with_comments_buffer(buffer, backend: :auto)
64
79
  parser = parser_for(backend: backend)
80
+ return nil unless parser
81
+
65
82
  parser.parse_with_comments(buffer)
83
+ rescue NoMethodError
84
+ nil
66
85
  end
67
86
 
68
87
  private
@@ -70,16 +89,16 @@ module Docscribe
70
89
  # Build the backend-specific parser object.
71
90
  #
72
91
  # @private
73
- # @param [Symbol] backend
74
- # @return [Object]
92
+ # @param [Symbol] backend requested backend
93
+ # @return [Parser::Base, nil]
75
94
  def parser_for(backend: :auto)
76
95
  case backend(backend)
77
96
  when :parser
78
97
  require 'parser/current'
79
- Parser::CurrentRuby.new
98
+ Parser::CurrentRuby.new # steep:ignore
80
99
  when :prism
81
100
  require 'prism'
82
- Prism::Translation::ParserCurrent.new
101
+ Prism::Translation::ParserCurrent.new # steep:ignore
83
102
  end
84
103
  end
85
104
 
@@ -93,7 +112,7 @@ module Docscribe
93
112
  # @private
94
113
  # @param [Symbol] backend requested backend
95
114
  # @raise [ArgumentError]
96
- # @return [Symbol] :parser or :prism
115
+ # @return [Symbol, nil] :parser or :prism
97
116
  def backend(backend = :auto)
98
117
  env = ENV.fetch('DOCSCRIBE_PARSER_BACKEND') { nil }
99
118
  backend = env.to_sym if env && !env.empty?
@@ -41,9 +41,9 @@ module Docscribe
41
41
  # - :anchor_node => Parser::AST::Node — node above which to insert doc
42
42
  # - :doc => String — complete doc block including newlines
43
43
  #
44
- # @param [Object] _ast Param documentation.
45
- # @param [Object] _buffer Param documentation.
46
- # @return [Array<Hash>]
44
+ # @param [Parser::AST::Node] _ast AST node to analyze
45
+ # @param [Parser::Source::Buffer] _buffer source buffer
46
+ # @return [Array<Hash<Symbol, Object>>]
47
47
  def collect(_ast, _buffer)
48
48
  []
49
49
  end
@@ -26,8 +26,7 @@ module Docscribe
26
26
  # Called once per documented method. Return [] if this plugin has
27
27
  # nothing to add for this particular method.
28
28
  #
29
- # @param [Docscribe::Plugin::Context] context method context snapshot
30
- # @param [Object] _context Param documentation.
29
+ # @param [Docscribe::Plugin::Context] _context method context snapshot (unused in default)
31
30
  # @return [Array<Docscribe::Plugin::Tag>]
32
31
  def call(_context)
33
32
  []
@@ -2,27 +2,37 @@
2
2
 
3
3
  module Docscribe
4
4
  module Plugin
5
- # Snapshot of everything known about a method at doc-generation time.
5
+ # @!attribute [rw] node
6
+ # @return [Parser::AST::Node]
7
+ # @param [Parser::AST::Node] value
6
8
  #
7
- # Passed to every registered TagPlugin. Read-only — plugins must not
8
- # mutate the context.
9
+ # @!attribute [rw] container
10
+ # @return [String]
11
+ # @param [String] value
9
12
  #
10
- # @!attribute node
11
- # @return [Parser::AST::Node] the :def or :defs AST node
12
- # @!attribute container
13
- # @return [String] e.g. "MyModule::MyClass" or "Object" for top-level
14
- # @!attribute scope
15
- # @return [Symbol] :instance or :class
16
- # @!attribute visibility
17
- # @return [Symbol] :public, :protected, or :private
18
- # @!attribute method_name
13
+ # @!attribute [rw] scope
19
14
  # @return [Symbol]
20
- # @!attribute inferred_params
21
- # @return [Hash{String => String}] name => inferred type
22
- # @!attribute inferred_return
23
- # @return [String] inferred return type
24
- # @!attribute source
25
- # @return [String] raw method source text
15
+ # @param [Symbol] value
16
+ #
17
+ # @!attribute [rw] visibility
18
+ # @return [Symbol]
19
+ # @param [Symbol] value
20
+ #
21
+ # @!attribute [rw] method_name
22
+ # @return [Symbol]
23
+ # @param [Symbol] value
24
+ #
25
+ # @!attribute [rw] inferred_params
26
+ # @return [Hash<String, String>]
27
+ # @param [Hash<String, String>] value
28
+ #
29
+ # @!attribute [rw] inferred_return
30
+ # @return [String]
31
+ # @param [String] value
32
+ #
33
+ # @!attribute [rw] source
34
+ # @return [String]
35
+ # @param [String] value
26
36
  Context = Struct.new(
27
37
  :node,
28
38
  :container,
@@ -16,12 +16,12 @@ module Docscribe
16
16
  # @param [Object] value
17
17
  #
18
18
  # @!attribute [rw] priority
19
- # @return [Object]
20
- # @param [Object] value
19
+ # @return [Integer]
20
+ # @param [Integer] value
21
21
  #
22
22
  # @!attribute [rw] order
23
- # @return [Object]
24
- # @param [Object] value
23
+ # @return [Integer]
24
+ # @param [Integer] value
25
25
  Entry = Struct.new(:plugin, :priority, :order, keyword_init: true)
26
26
 
27
27
  @tag_entries = []
@@ -38,23 +38,49 @@ module Docscribe
38
38
  # - responds to #call => tag plugin (duck typing)
39
39
  # - responds to #collect => collector plugin (duck typing)
40
40
  #
41
- # @note module_function: when included, also defines #register (instance visibility: private)
41
+ # @note module_function: defines #register (visibility: private)
42
42
  # @param [Object] plugin plugin instance
43
43
  # @param [Integer] priority plugin priority (higher wins for conflicts)
44
- # @raise [ArgumentError] if plugin type cannot be determined
45
- # @raise [StandardError]
46
44
  # @return [void]
47
45
  def register(plugin, priority: 0)
48
- prio =
49
- begin
50
- Integer(priority)
51
- rescue StandardError
52
- raise ArgumentError, "priority must be an Integer-like value, got: #{priority.inspect}"
53
- end
46
+ prio = parse_priority(priority)
47
+ entry = create_entry(plugin, prio)
48
+ route_entry(entry, plugin)
49
+ end
54
50
 
51
+ # Parse and validate plugin priority.
52
+ #
53
+ # @note module_function: defines #parse_priority (visibility: private)
54
+ # @param [String, Integer] priority plugin priority (higher wins for conflicts)
55
+ # @raise [StandardError]
56
+ # @raise [ArgumentError]
57
+ # @return [Integer] if StandardError
58
+ # @return [Object] if StandardError
59
+ def parse_priority(priority)
60
+ Integer(priority)
61
+ rescue StandardError
62
+ raise ArgumentError, "priority must be an Integer-like value, got: #{priority.inspect}"
63
+ end
64
+
65
+ # Create a new Entry with the next order number.
66
+ #
67
+ # @note module_function: defines #create_entry (visibility: private)
68
+ # @param [Object] plugin plugin instance
69
+ # @param [Integer] priority plugin priority (higher wins for conflicts)
70
+ # @return [Docscribe::Plugin::Registry::Entry]
71
+ def create_entry(plugin, priority)
55
72
  @order_seq += 1
56
- entry = Entry.new(plugin: plugin, priority: prio, order: @order_seq)
73
+ Entry.new(plugin: plugin, priority: priority, order: @order_seq)
74
+ end
57
75
 
76
+ # Route entry to tag or collector list.
77
+ #
78
+ # @note module_function: defines #route_entry (visibility: private)
79
+ # @param [Docscribe::Plugin::Registry::Entry] entry the entry to route
80
+ # @param [Object] plugin plugin instance
81
+ # @raise [ArgumentError]
82
+ # @return [void]
83
+ def route_entry(entry, plugin)
58
84
  if plugin.is_a?(Base::CollectorPlugin) || plugin.respond_to?(:collect)
59
85
  @collector_entries << entry
60
86
  elsif plugin.is_a?(Base::TagPlugin) || plugin.respond_to?(:call)
@@ -66,32 +92,32 @@ module Docscribe
66
92
 
67
93
  # All registered tag plugins in registration order.
68
94
  #
69
- # @note module_function: when included, also defines #tag_plugins (instance visibility: private)
70
- # @return [Array<#call>]
95
+ # @note module_function: defines #tag_plugins (visibility: private)
96
+ # @return [Array<Object>]
71
97
  def tag_plugins
72
98
  @tag_entries.map(&:plugin)
73
99
  end
74
100
 
75
101
  # All registered collector plugins in registration order.
76
102
  #
77
- # @note module_function: when included, also defines #collector_plugins (instance visibility: private)
78
- # @return [Array<#collect>]
103
+ # @note module_function: defines #collector_plugins (visibility: private)
104
+ # @return [Array<Object>]
79
105
  def collector_plugins
80
106
  @collector_entries.map(&:plugin)
81
107
  end
82
108
 
83
109
  # All registered tag plugin entries (plugin + priority metadata).
84
110
  #
85
- # @note module_function: when included, also defines #tag_entries (instance visibility: private)
86
- # @return [Array<Entry>]
111
+ # @note module_function: defines #tag_entries (visibility: private)
112
+ # @return [Array<Docscribe::Plugin::Registry::Entry>]
87
113
  def tag_entries
88
114
  @tag_entries.dup
89
115
  end
90
116
 
91
117
  # All registered collector plugin entries (plugin + priority metadata).
92
118
  #
93
- # @note module_function: when included, also defines #collector_entries (instance visibility: private)
94
- # @return [Array<Entry>]
119
+ # @note module_function: defines #collector_entries (visibility: private)
120
+ # @return [Array<Docscribe::Plugin::Registry::Entry>]
95
121
  def collector_entries
96
122
  @collector_entries.dup
97
123
  end
@@ -100,7 +126,7 @@ module Docscribe
100
126
  #
101
127
  # Primarily used in tests to reset state between examples.
102
128
  #
103
- # @note module_function: when included, also defines #clear! (instance visibility: private)
129
+ # @note module_function: defines #clear! (visibility: private)
104
130
  # @return [void]
105
131
  def clear!
106
132
  @tag_entries.clear
@@ -2,22 +2,17 @@
2
2
 
3
3
  module Docscribe
4
4
  module Plugin
5
- # A single YARD-style tag returned by a TagPlugin.
5
+ # @!attribute [rw] name
6
+ # @return [String]
7
+ # @param [String] value
6
8
  #
7
- # @example Simple tag
8
- # Tag.new(name: 'since', text: '1.3.0')
9
- # # => # @since 1.3.0
9
+ # @!attribute [rw] text
10
+ # @return [String, nil]
11
+ # @param [String, nil] value
10
12
  #
11
- # @example Tag with types
12
- # Tag.new(name: 'raise', types: ['ArgumentError'], text: 'if name is nil')
13
- # # => # @raise [ArgumentError] if name is nil
14
- #
15
- # @!attribute name
16
- # @return [String] tag name without leading @
17
- # @!attribute text
18
- # @return [String, nil] text after the type bracket
19
- # @!attribute types
20
- # @return [Array<String>, nil] optional type list rendered as [Foo, Bar]
13
+ # @!attribute [rw] types
14
+ # @return [Array<String>, nil]
15
+ # @param [Array<String>, nil] value
21
16
  Tag = Struct.new(:name, :text, :types, keyword_init: true)
22
17
  end
23
18
  end
@@ -23,7 +23,7 @@ module Docscribe
23
23
  # Errors in individual plugins are caught so one broken plugin does not
24
24
  # abort the entire run.
25
25
  #
26
- # @param [Docscribe::Plugin::Context] context
26
+ # @param [Docscribe::Plugin::Context] context plugin execution context
27
27
  # @raise [StandardError]
28
28
  # @return [Array<Docscribe::Plugin::Tag>]
29
29
  def self.run_tag_plugins(context)
@@ -43,32 +43,64 @@ module Docscribe
43
43
 
44
44
  # Run all registered CollectorPlugins for one file's AST.
45
45
  #
46
- # @param [Parser::AST::Node] ast
47
- # @param [Parser::Source::Buffer] buffer
48
- # @raise [StandardError]
49
- # @return [Array<Hash>]
46
+ # @param [Parser::AST::Node] ast parsed AST root node
47
+ # @param [Parser::Source::Buffer] buffer source buffer for AST
48
+ # @return [Array<Hash<Symbol, Object>>]
50
49
  def self.run_collector_plugins(ast, buffer)
51
- Registry.collector_entries.flat_map do |entry|
52
- plugin = entry.plugin
50
+ Registry.collector_entries.flat_map { |entry| process_single_plugin_result(entry, ast, buffer) }
51
+ end
53
52
 
54
- Array(plugin.collect(ast, buffer)).map do |insertion|
55
- unless insertion.is_a?(Hash)
56
- warn "Docscribe: CollectorPlugin #{plugin.class} returned #{insertion.class}, expected Hash" if debug?
57
- next nil
58
- end
53
+ # Process a single collector plugin's result.
54
+ #
55
+ # Merges plugin metadata into each hash insertion and handles errors.
56
+ #
57
+ # @param [Docscribe::Plugin::Registry::Entry] entry registry entry with priority and order metadata
58
+ # @param [Parser::AST::Node] ast parsed AST root node
59
+ # @param [Parser::Source::Buffer] buffer source buffer for AST
60
+ # @raise [StandardError]
61
+ # @return [Array<Hash<Symbol, Object>>] if StandardError
62
+ # @return [Array] if StandardError
63
+ def self.process_single_plugin_result(entry, ast, buffer)
64
+ plugin = entry.plugin
65
+ results = Array(plugin.collect(ast, buffer))
66
+ process_plugin_insertions(results, entry, plugin)
67
+ rescue StandardError => e
68
+ warn "Docscribe: CollectorPlugin #{plugin.class} raised #{e.class}: #{e.message}" if debug?
69
+ []
70
+ end
59
71
 
60
- insertion.merge(
61
- __docscribe_priority: entry.priority,
62
- __docscribe_plugin_class: plugin.class.name,
63
- __docscribe_plugin_order: entry.order
64
- )
65
- end.compact
66
- rescue StandardError => e
67
- warn "Docscribe: CollectorPlugin #{plugin.class} raised #{e.class}: #{e.message}" if debug?
68
- []
69
- end
72
+ # Merge plugin metadata into collector results and filter invalid ones.
73
+ #
74
+ # @param [Array<Hash<Symbol, Object>>] results collector plugin results to process
75
+ # @param [Docscribe::Plugin::Registry::Entry] entry registry entry with priority and order metadata
76
+ # @param [Object] plugin the collector plugin instance
77
+ # @return [Array<Hash<Symbol, Object>>]
78
+ def self.process_plugin_insertions(results, entry, plugin)
79
+ results.map do |insertion|
80
+ next nil unless valid_plugin_result?(insertion, plugin)
81
+
82
+ insertion.merge(
83
+ __docscribe_priority: entry.priority,
84
+ __docscribe_plugin_class: plugin.class.name,
85
+ __docscribe_plugin_order: entry.order
86
+ )
87
+ end.compact
70
88
  end
71
89
 
90
+ # Validate a CollectorPlugin result is a Hash.
91
+ #
92
+ # @param [Object] insertion collector plugin result
93
+ # @param [Object] plugin the collector plugin instance
94
+ # @return [Boolean]
95
+ def self.valid_plugin_result?(insertion, plugin)
96
+ return true if insertion.is_a?(Hash)
97
+
98
+ warn "Docscribe: CollectorPlugin #{plugin.class} returned #{insertion.class}, expected Hash" if debug?
99
+ false
100
+ end
101
+
102
+ # Self
103
+ #
72
104
  # @return [Boolean]
73
105
  def self.debug?
74
106
  ENV['DOCSCRIBE_DEBUG'] == '1'
@@ -12,8 +12,10 @@ module Docscribe
12
12
  # - Sorbet RBI files
13
13
  # - RBS files
14
14
  class ProviderChain
15
- # @param [Array<#signature_for>] providers ordered signature providers
16
- # @return [Object]
15
+ # Initialize
16
+ #
17
+ # @param [Array<Docscribe::Types::_Provider>] providers ordered signature providers
18
+ # @return [void]
17
19
  def initialize(*providers)
18
20
  @providers = providers.compact
19
21
  end
@@ -1,4 +1,3 @@
1
- # lib/docscribe/types/rbs/collection_loader.rb
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require 'pathname'
@@ -31,14 +30,14 @@ module Docscribe
31
30
  # - lock-file is absent (collection not initialized)
32
31
  # - resolved directory does not exist on disk (collection not installed)
33
32
  #
34
- # @note module_function: when included, also defines #resolve (instance visibility: private)
33
+ # @note module_function: defines #resolve (visibility: private)
35
34
  # @param [String] root project root to search from
36
35
  # @return [String, nil] absolute path to the collection directory, or nil
37
36
  def resolve(root: Dir.pwd)
38
37
  lock = Pathname(root).join(LOCK_FILE)
39
38
  return nil unless lock.file?
40
39
 
41
- data = YAML.safe_load(lock.read, permitted_classes: [Symbol]) || {}
40
+ data = YAML.safe_load(lock.read, permitted_classes: [Symbol]) || {} # steep:ignore
42
41
  rel = data['path'] || DEFAULT_COLLECTION_PATH
43
42
 
44
43
  resolved = Pathname(root).join(rel)