docscribe 1.6.1 → 1.6.2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -193
  3. data/exe/docscribe-client +26 -7
  4. data/lib/docscribe/cli/config_builder.rb +37 -2
  5. data/lib/docscribe/cli/coverage.rb +5 -5
  6. data/lib/docscribe/cli/formatters/json.rb +74 -29
  7. data/lib/docscribe/cli/formatters/sarif.rb +20 -3
  8. data/lib/docscribe/cli/options.rb +17 -2
  9. data/lib/docscribe/cli/rbs_gen.rb +4 -4
  10. data/lib/docscribe/cli/run.rb +107 -24
  11. data/lib/docscribe/cli/update_types.rb +61 -17
  12. data/lib/docscribe/cli.rb +19 -13
  13. data/lib/docscribe/config/defaults.rb +1 -0
  14. data/lib/docscribe/config/rbs.rb +22 -1
  15. data/lib/docscribe/config/template.rb +3 -0
  16. data/lib/docscribe/config/validation.rb +19 -0
  17. data/lib/docscribe/config.rb +1 -0
  18. data/lib/docscribe/infer/behavior.rb +13 -13
  19. data/lib/docscribe/infer/params.rb +2 -2
  20. data/lib/docscribe/infer/raises.rb +5 -6
  21. data/lib/docscribe/infer/returns.rb +1612 -151
  22. data/lib/docscribe/infer.rb +7 -7
  23. data/lib/docscribe/inline_rewriter/doc_builder.rb +485 -102
  24. data/lib/docscribe/inline_rewriter.rb +263 -97
  25. data/lib/docscribe/plugin/registry.rb +1 -0
  26. data/lib/docscribe/server/base.rb +46 -15
  27. data/lib/docscribe/server/client.rb +20 -11
  28. data/lib/docscribe/server/daemon.rb +200 -23
  29. data/lib/docscribe/server/protocol.rb +4 -4
  30. data/lib/docscribe/types/primitive.rb +160 -0
  31. data/lib/docscribe/types/sorbet/base_provider.rb +33 -1
  32. data/lib/docscribe/types/yard/formatter.rb +35 -6
  33. data/lib/docscribe/types/yard/parser.rb +25 -20
  34. data/lib/docscribe/types/yard/validator.rb +131 -0
  35. data/lib/docscribe/validator/generic_compatibility.rb +698 -0
  36. data/lib/docscribe/validator/type_mismatch_validator.rb +287 -0
  37. data/lib/docscribe/version.rb +1 -1
  38. metadata +8 -3
@@ -10,7 +10,7 @@ module Docscribe
10
10
  # Two-pass update: rebuild docs then re-merge with RBS types.
11
11
  #
12
12
  # Usage:
13
- # docscribe update_types [directory]
13
+ # docscribe update_types [directory|file]
14
14
  #
15
15
  # Pass 1: `-AkB --rbs-collection <dir>` — aggressive rebuild, keep descriptions,
16
16
  # no boilerplate, using RBS collection signatures.
@@ -18,7 +18,7 @@ module Docscribe
18
18
  # using RBS collection signatures.
19
19
  module UpdateTypes
20
20
  BANNER = <<~TEXT
21
- Usage: docscribe update_types [directory]
21
+ Usage: docscribe update_types [directory|file] [options]
22
22
 
23
23
  Two-pass type-aware documentation update.
24
24
 
@@ -28,6 +28,8 @@ module Docscribe
28
28
  Pass 2 (safe): docscribe -aB --rbs-collection <dir>
29
29
  safe merge cleanup, no boilerplate
30
30
 
31
+ See `docscribe --help` for type options (--rbs, --sig-dir, --rbs-collection, --[no-]validate-types).
32
+
31
33
  TEXT
32
34
 
33
35
  class << self
@@ -35,14 +37,15 @@ module Docscribe
35
37
  # @return [Integer]
36
38
  def run(argv)
37
39
  options = parse_options(argv)
38
- dir = options[:dir]
40
+ target = options[:dir]
41
+ @extra_argv = options[:extra_argv]
39
42
 
40
43
  announce_start
41
44
 
42
- exit1 = run_first_pass(dir)
45
+ exit1 = run_first_pass(target)
43
46
  return exit1 unless exit1.zero?
44
47
 
45
- exit2 = run_second_pass(dir)
48
+ exit2 = run_second_pass(target)
46
49
  return exit2 unless exit2.zero?
47
50
 
48
51
  announce_complete
@@ -54,13 +57,28 @@ module Docscribe
54
57
  # @private
55
58
  # @param [Array<String>] argv
56
59
  # @return [Hash<Symbol, Object>]
57
- def parse_options(argv)
58
- options = { dir: '.' }
60
+ def parse_options(argv) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
61
+ options = { dir: '.', extra_argv: [] }
62
+ extra_argv = []
59
63
  OptionParser.new(BANNER) do |opts|
60
64
  opts.on('-h', '--help', 'Show this help') { puts opts or exit 0 }
65
+ opts.on('--[no-]rbs', 'Use RBS signatures when available') do |v|
66
+ extra_argv << (v ? '--rbs' : '--no-rbs')
67
+ end
68
+ opts.on('--sig-dir DIR', 'Add an RBS signature directory (repeatable). Implies --rbs.') do |v|
69
+ extra_argv << '--sig-dir' << v
70
+ end
71
+ opts.on('--rbs-collection', 'Auto-discover RBS collection from rbs_collection.lock.yaml. Implies --rbs.') do
72
+ extra_argv << '--rbs-collection'
73
+ end
74
+ opts.on('--[no-]validate-types', 'Validate YARD types against inferred/RBS types (optional, default: off)') do |v|
75
+ extra_argv << (v ? '--validate-types' : '--no-validate-types')
76
+ end
61
77
  opts.parse!(argv)
62
78
  end
63
79
  options[:dir] = argv.first if argv.any?
80
+ options[:extra_argv] = extra_argv
81
+ @extra_argv = extra_argv
64
82
  options
65
83
  end
66
84
 
@@ -72,23 +90,49 @@ module Docscribe
72
90
  end
73
91
 
74
92
  # @private
75
- # @param [String] dir
93
+ # @param [String] target
76
94
  # @return [Integer]
77
- def run_first_pass(dir)
78
- puts 'Pass 1: Aggressive rebuild with RBS collection...'
79
- argv1 = ['-AkB', '--rbs-collection', dir]
95
+ def run_first_pass(target) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
96
+ dir_for_flag = File.file?(target) ? File.dirname(target) : target
97
+ has_collection = File.exist?(File.join(dir_for_flag, 'rbs_collection.lock.yaml')) || File.exist?('rbs_collection.lock.yaml')
98
+ flag = has_collection ? '--rbs-collection' : '--rbs'
99
+ extra = @extra_argv || []
100
+ filtered = extra.reject { |a| a == '--no-rbs' }
101
+ has_rbs_flag = extra.any? { |a| %w[--rbs --rbs-collection --no-rbs].include?(a) }
102
+ has_no_rbs = extra.include?('--no-rbs')
103
+ argv1 = ['-AkB']
104
+ argv1.concat(filtered)
105
+ if has_rbs_flag
106
+ argv1 << dir_for_flag unless has_no_rbs || argv1.include?(dir_for_flag)
107
+ else
108
+ argv1 << flag << dir_for_flag
109
+ end
110
+ puts "Pass 1: Aggressive rebuild with #{has_collection ? 'RBS collection' : 'RBS'}#{' + validate-types' if extra.include?('--validate-types')}..."
80
111
  options1 = Docscribe::CLI::Options.parse!(argv1)
81
- Docscribe::CLI::Run.run(options: options1, argv: [dir])
112
+ Docscribe::CLI::Run.run(options: options1, argv: [target])
82
113
  end
83
114
 
84
115
  # @private
85
- # @param [String] dir
116
+ # @param [String] target
86
117
  # @return [Integer]
87
- def run_second_pass(dir)
88
- puts 'Pass 2: Safe merge with RBS collection...'
89
- argv2 = ['-aB', '--rbs-collection', dir]
118
+ def run_second_pass(target) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
119
+ dir_for_flag = File.file?(target) ? File.dirname(target) : target
120
+ has_collection = File.exist?(File.join(dir_for_flag, 'rbs_collection.lock.yaml')) || File.exist?('rbs_collection.lock.yaml')
121
+ flag = has_collection ? '--rbs-collection' : '--rbs'
122
+ extra = @extra_argv || []
123
+ filtered = extra.reject { |a| a == '--no-rbs' }
124
+ has_rbs_flag = extra.any? { |a| %w[--rbs --rbs-collection --no-rbs].include?(a) }
125
+ has_no_rbs = extra.include?('--no-rbs')
126
+ argv2 = ['-aB']
127
+ argv2.concat(filtered)
128
+ if has_rbs_flag
129
+ argv2 << dir_for_flag unless has_no_rbs || argv2.include?(dir_for_flag)
130
+ else
131
+ argv2 << flag << dir_for_flag
132
+ end
133
+ puts "Pass 2: Safe merge with #{has_collection ? 'RBS collection' : 'RBS'}#{' + validate-types' if extra.include?('--validate-types')}..."
90
134
  options2 = Docscribe::CLI::Options.parse!(argv2)
91
- Docscribe::CLI::Run.run(options: options2, argv: [dir])
135
+ Docscribe::CLI::Run.run(options: options2, argv: [target])
92
136
  end
93
137
 
94
138
  # @private
data/lib/docscribe/cli.rb CHANGED
@@ -6,6 +6,24 @@ require 'docscribe/cli/run'
6
6
  module Docscribe
7
7
  # CLI entry point and command dispatch.
8
8
  module CLI
9
+ COMMANDS = {
10
+ 'check_for_comments' => :CheckForComments,
11
+ 'config' => :ConfigDump,
12
+ 'coverage' => :Coverage,
13
+ 'generate' => :Generate,
14
+ 'init' => :Init,
15
+ 'rbs' => :RbsGen,
16
+ 'server' => :ServerCmd,
17
+ 'sigs' => :Sigs,
18
+ 'update_types' => :UpdateTypes
19
+ }.freeze
20
+
21
+ # Subcommands whose file name differs from the command name.
22
+ SUBCOMMAND_FILES = {
23
+ 'config' => 'config_dump',
24
+ 'rbs' => 'rbs_gen'
25
+ }.freeze
26
+
9
27
  class << self
10
28
  # @param [Array<String>] argv
11
29
  # @return [Integer]
@@ -17,18 +35,6 @@ module Docscribe
17
35
  Docscribe::CLI::Run.run(options: options, argv: argv)
18
36
  end
19
37
 
20
- COMMANDS = {
21
- 'check_for_comments' => :CheckForComments,
22
- 'config' => :ConfigDump,
23
- 'coverage' => :Coverage,
24
- 'generate' => :Generate,
25
- 'init' => :Init,
26
- 'rbs' => :RbsGen,
27
- 'server' => :ServerCmd,
28
- 'sigs' => :Sigs,
29
- 'update_types' => :UpdateTypes
30
- }.freeze
31
-
32
38
  private
33
39
 
34
40
  # @private
@@ -46,7 +52,7 @@ module Docscribe
46
52
  const_name = COMMANDS[cmd]
47
53
  return 0 unless const_name
48
54
 
49
- require "docscribe/cli/#{cmd == 'rbs' ? 'rbs_gen' : cmd}"
55
+ require "docscribe/cli/#{SUBCOMMAND_FILES.fetch(cmd) { cmd }}"
50
56
  Docscribe::CLI.const_get(const_name).run(argv)
51
57
  end
52
58
  end
@@ -75,6 +75,7 @@ module Docscribe
75
75
  },
76
76
  'keep_descriptions' => false,
77
77
  'skip_anonymous_block_params' => false,
78
+ 'validate_types' => false,
78
79
  'plugins' => {
79
80
  'require' => [] #: Array[String]
80
81
  }
@@ -107,10 +107,31 @@ module Docscribe
107
107
  # RBS environment errors (e.g. duplicate declarations against core
108
108
  # stdlib types) do not silence all RBS lookups.
109
109
  #
110
+ # When no explicit dirs are configured but `rbs.collection: true` is
111
+ # set, the lock file is auto-discovered (same as `--rbs-collection`),
112
+ # so plain `check` gets the full environment without extra flags.
113
+ #
110
114
  # @private
111
115
  # @return [Array<String>]
112
116
  def rbs_collection_dirs
113
- Array(raw.dig('rbs', 'collection_dirs')).map(&:to_s) # steep:ignore
117
+ explicit = Array(raw.dig('rbs', 'collection_dirs')).map(&:to_s) # steep:ignore
118
+ return explicit unless explicit.empty?
119
+ return [] unless raw.dig('rbs', 'collection')
120
+
121
+ Array(discovered_collection_dir).compact
122
+ end
123
+
124
+ # Resolve the collection directory from rbs_collection.lock.yaml.
125
+ #
126
+ # @private
127
+ # @raise [LoadError]
128
+ # @return [String, nil] resolved directory or nil when no lock file
129
+ # @return [nil] if LoadError
130
+ def discovered_collection_dir
131
+ require 'docscribe/types/rbs/collection_loader'
132
+ Docscribe::Types::RBS::CollectionLoader.resolve
133
+ rescue LoadError
134
+ nil
114
135
  end
115
136
 
116
137
  # Whether generic RBS types should be collapsed to simpler container names.
@@ -106,6 +106,9 @@ module Docscribe
106
106
  # Skip @param for anonymous block arguments (&) (Ruby 3.2+)
107
107
  skip_anonymous_block_params: false
108
108
 
109
+ # Validate YARD types against inferred/RBS types
110
+ validate_types: false
111
+
109
112
  plugins:
110
113
  # Load custom plugins
111
114
  # Example:
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ # Validation-related configuration.
5
+ class Config
6
+ # Whether to validate YARD types against inferred / external types.
7
+ #
8
+ # When enabled, `docscribe lib --validate-types` (or `validate_types: true`
9
+ # in `docscribe.yml`) compares each `@param`/`@return` type written in YARD
10
+ # against the type inferred from the method body or provided via RBS/Sorbet.
11
+ # Mismatches are reported as `updated_param` / `updated_return` and, in
12
+ # check mode, cause exit 1.
13
+ #
14
+ # @return [Boolean]
15
+ def validate_types?
16
+ fetch_bool(%w[validate_types], false)
17
+ end
18
+ end
19
+ end
@@ -49,3 +49,4 @@ require_relative 'config/rbs'
49
49
  require_relative 'config/sorting'
50
50
  require_relative 'config/sorbet'
51
51
  require_relative 'config/plugin'
52
+ require_relative 'config/validation'
@@ -8,9 +8,9 @@ module Docscribe
8
8
  insert update_all delete_all].freeze
9
9
 
10
10
  class << self
11
- # @param [Object] body
12
- # @param [Object] method_name
13
- # @return [Hash<Symbol, Object>]
11
+ # @param [Parser::AST::Node, nil] body
12
+ # @param [Symbol?] method_name
13
+ # @return [Hash<Symbol, Boolean, String, nil>]
14
14
  def analyze(body, method_name)
15
15
  result = default_result(method_name)
16
16
 
@@ -20,8 +20,8 @@ module Docscribe
20
20
  result
21
21
  end
22
22
 
23
- # @param [Object] method_name
24
- # @return [Hash<Symbol, Object>]
23
+ # @param [Symbol?] method_name
24
+ # @return [Hash<Symbol, Boolean, String, nil>]
25
25
  def default_result(method_name)
26
26
  {
27
27
  predicate: method_name&.to_s&.end_with?('?') || false,
@@ -33,8 +33,8 @@ module Docscribe
33
33
  }
34
34
  end
35
35
 
36
- # @param [Hash<Symbol, Object>] analysis
37
- # @param [Object] _method_name
36
+ # @param [Hash<Symbol, Boolean, String, nil>] analysis
37
+ # @param [Symbol?] _method_name
38
38
  # @return [String?]
39
39
  def infer_description(analysis, _method_name)
40
40
  return nil unless analysis[:has_side_effects] || analysis[:predicate]
@@ -51,8 +51,8 @@ module Docscribe
51
51
  private
52
52
 
53
53
  # @private
54
- # @param [Object] node
55
- # @param [Hash<Symbol, Object>] result
54
+ # @param [Parser::AST::Node] node
55
+ # @param [Hash<Symbol, Boolean, String, nil>] result
56
56
  # @return [void]
57
57
  def analyze_body(node, result)
58
58
  case node.type
@@ -68,8 +68,8 @@ module Docscribe
68
68
  end
69
69
 
70
70
  # @private
71
- # @param [Object] node
72
- # @param [Hash<Symbol, Object>] result
71
+ # @param [Parser::AST::Node] node
72
+ # @param [Hash<Symbol, Boolean, String, nil>] result
73
73
  # @return [void]
74
74
  def recurse_children(node, result)
75
75
  node.children.each do |child|
@@ -78,8 +78,8 @@ module Docscribe
78
78
  end
79
79
 
80
80
  # @private
81
- # @param [Object] node
82
- # @param [Hash<Symbol, Object>] result
81
+ # @param [Parser::AST::Node] node
82
+ # @param [Hash<Symbol, Boolean, String, nil>] result
83
83
  # @return [void]
84
84
  def analyze_send(node, result)
85
85
  _receiver, method_name = *node
@@ -33,8 +33,8 @@ module Docscribe
33
33
  # @return [String, nil]
34
34
  def prefix_param_type(name)
35
35
  return 'Array' if name.start_with?('*') && !name.start_with?('**')
36
- return 'Hash' if name.start_with?('**')
37
- return 'Proc' if name.start_with?('&')
36
+ return 'Hash' if name.start_with?('**')
37
+ return 'Proc' if name.start_with?('&')
38
38
 
39
39
  nil
40
40
  end
@@ -23,10 +23,9 @@ module Docscribe
23
23
  raises = [] #: Array[String]
24
24
 
25
25
  ASTWalk.walk(node) do |n|
26
- case n.type
27
- when :resbody
26
+ if n&.type == :resbody
28
27
  raises.concat(exception_names_from_rescue_list(n.children[0]))
29
- when :send
28
+ elsif n&.type == :send
30
29
  collect_send_raise(raises, n)
31
30
  end
32
31
  end
@@ -45,9 +44,9 @@ module Docscribe
45
44
  # @param [Parser::AST::Node, nil] exc_list rescue exception list node
46
45
  # @return [Array<String>]
47
46
  def exception_names_from_rescue_list(exc_list)
48
- if exc_list.nil?
49
- [DEFAULT_ERROR]
50
- elsif exc_list.type == :array
47
+ return [DEFAULT_ERROR] unless exc_list.is_a?(Parser::AST::Node)
48
+
49
+ if exc_list.type == :array
51
50
  exc_list.children.map { |e| Names.const_full_name(e) || DEFAULT_ERROR }
52
51
  else
53
52
  [Names.const_full_name(exc_list) || DEFAULT_ERROR]