docscribe 1.5.2 → 1.6.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.
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Infer
5
+ # Analyzes method behavior (predicates, side effects, mutating calls).
6
+ module Behavior
7
+ MUTATING_METHODS = %i[<< push pop delete merge update write save create destroy
8
+ insert update_all delete_all].freeze
9
+
10
+ class << self
11
+ # @param [Object] body
12
+ # @param [Object] method_name
13
+ # @return [Hash<Symbol, Object>]
14
+ def analyze(body, method_name)
15
+ result = default_result(method_name)
16
+
17
+ return result unless body.is_a?(Parser::AST::Node)
18
+
19
+ analyze_body(body, result)
20
+ result
21
+ end
22
+
23
+ # @param [Object] method_name
24
+ # @return [Hash<Symbol, Object>]
25
+ def default_result(method_name)
26
+ {
27
+ predicate: method_name&.to_s&.end_with?('?') || false,
28
+ bang: method_name&.to_s&.end_with?('!') || false,
29
+ has_side_effects: false,
30
+ delegates: nil,
31
+ returns_self: false,
32
+ returns_boolean: false
33
+ }
34
+ end
35
+
36
+ # @param [Hash<Symbol, Object>] analysis
37
+ # @param [Object] _method_name
38
+ # @return [String?]
39
+ def infer_description(analysis, _method_name)
40
+ return nil unless analysis[:has_side_effects] || analysis[:predicate]
41
+
42
+ if analysis[:predicate]
43
+ 'Returns true if the condition is met, false otherwise'
44
+ elsif analysis[:returns_self]
45
+ 'Returns self to allow method chaining'
46
+ elsif analysis[:has_side_effects]
47
+ nil
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ # @private
54
+ # @param [Object] node
55
+ # @param [Hash<Symbol, Object>] result
56
+ # @return [void]
57
+ def analyze_body(node, result)
58
+ case node.type
59
+ when :ivasgn, :ivar
60
+ result[:has_side_effects] = true
61
+ when :send
62
+ analyze_send(node, result)
63
+ when :self
64
+ result[:returns_self] = true if result[:returns_self]
65
+ end
66
+
67
+ recurse_children(node, result)
68
+ end
69
+
70
+ # @private
71
+ # @param [Object] node
72
+ # @param [Hash<Symbol, Object>] result
73
+ # @return [void]
74
+ def recurse_children(node, result)
75
+ node.children.each do |child|
76
+ analyze_body(child, result) if child.is_a?(Parser::AST::Node)
77
+ end
78
+ end
79
+
80
+ # @private
81
+ # @param [Object] node
82
+ # @param [Hash<Symbol, Object>] result
83
+ # @return [void]
84
+ def analyze_send(node, result)
85
+ _receiver, method_name = *node
86
+ method_sym = method_name.is_a?(Symbol) ? method_name : nil
87
+ return unless method_sym
88
+
89
+ return unless MUTATING_METHODS.include?(method_sym)
90
+
91
+ result[:has_side_effects] = true
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -48,9 +48,7 @@ module Docscribe
48
48
  # @param [Boolean] treat_options_keyword_as_hash whether to treat 'options:' as Hash
49
49
  # @return [String]
50
50
  def inferred_param_type(name, default_str, fallback_type, treat_options_keyword_as_hash:)
51
- if name.end_with?(':') && default_str.nil?
52
- return options_keyword_type(name, treat_options_keyword_as_hash, fallback_type)
53
- end
51
+ return options_keyword_type(name, treat_options_keyword_as_hash, fallback_type) if name.end_with?(':') && default_str.nil?
54
52
 
55
53
  node = parse_expr(default_str)
56
54
  ty = Literals.type_from_literal(node, fallback_type: fallback_type)
@@ -16,6 +16,7 @@ require_relative 'infer/literals'
16
16
  require_relative 'infer/params'
17
17
  require_relative 'infer/returns'
18
18
  require_relative 'infer/raises'
19
+ require_relative 'infer/behavior'
19
20
 
20
21
  module Docscribe
21
22
  # Best-effort inference utilities used to generate YARD tags.
@@ -37,6 +38,40 @@ module Docscribe
37
38
  Raises.infer_raises_from_node(node)
38
39
  end
39
40
 
41
+ # Analyze method behavior from AST node and method name.
42
+ #
43
+ # @param [Object] node def or defs node
44
+ # @param [Object] method_name
45
+ # @return [Hash<Symbol, Object>]
46
+ def analyze_behavior(node, method_name)
47
+ body = extract_body(node)
48
+ Behavior.analyze(body, method_name)
49
+ end
50
+
51
+ # Get behavioral description for a method.
52
+ #
53
+ # @param [Object] node def or defs node
54
+ # @param [Object] method_name
55
+ # @return [String?]
56
+ def infer_behavior_description(node, method_name)
57
+ body = extract_body(node)
58
+ return nil unless body
59
+
60
+ analysis = Behavior.analyze(body, method_name)
61
+ Behavior.infer_description(analysis, method_name)
62
+ end
63
+
64
+ # Extract method body from def/defs node.
65
+ #
66
+ # @param [Object] node def or defs node
67
+ # @return [Object?]
68
+ def extract_body(node)
69
+ return nil unless node.is_a?(Parser::AST::Node)
70
+
71
+ body_idx = node.type == :def ? 2 : 3
72
+ node.children[body_idx]
73
+ end
74
+
40
75
  # Infer a parameter type from its internal name form and optional default
41
76
  # expression.
42
77
  #
@@ -186,7 +186,7 @@ module Docscribe
186
186
  # @param [Hash<Symbol, Object>] opts additional options including
187
187
  # @return [Hash<Symbol, Object>]
188
188
  def resolve_doc_setup!(setup, node, name, config, opts)
189
- external_sig = resolve_external_sig(setup[:container], setup[:scope], name, opts[:signature_provider])
189
+ external_sig = resolve_external_sig(setup[:container], setup[:scope], name, opts[:signature_provider], node)
190
190
  returns_spec = compute_returns_spec(node, config, opts[:param_types], opts[:core_rbs_provider],
191
191
  signature_provider: opts[:signature_provider],
192
192
  container: setup[:container])
@@ -219,9 +219,26 @@ module Docscribe
219
219
  # @param [Symbol] scope method scope symbol
220
220
  # @param [Symbol] name the method name string
221
221
  # @param [Docscribe::Types::ProviderChain, nil] signature_provider external sig provider
222
+ # @param [Parser::AST::Node] node
222
223
  # @return [Docscribe::Types::MethodSignature, nil]
223
- def resolve_external_sig(container, scope, name, signature_provider)
224
- signature_provider&.signature_for(container: container, scope: scope, name: name)
224
+ def resolve_external_sig(container, scope, name, signature_provider, node = nil)
225
+ param_count, param_names = extract_sig_param_info(node)
226
+ signature_provider&.signature_for(container: container, scope: scope, name: name,
227
+ param_count: param_count, param_names: param_names)
228
+ end
229
+
230
+ # @note module_function: defines #extract_sig_param_info (visibility: private)
231
+ # @param [Parser::AST::Node?] node
232
+ # @return [(Integer?, Array<String>)]
233
+ def extract_sig_param_info(node)
234
+ empty = [] #: Array[String]
235
+ return [nil, empty] unless node
236
+
237
+ args = extract_args_from_node(node)
238
+ empty = [] #: Array[String]
239
+ return [nil, empty] unless args
240
+
241
+ [args.children.length, args.children.map { |a| a.children.first.to_s if a.respond_to?(:children) }.compact]
225
242
  end
226
243
 
227
244
  # Compute returns spec
@@ -832,9 +849,7 @@ module Docscribe
832
849
  # @param [Hash<Symbol, Object>] info parse info hash to update with visibility flags
833
850
  # @return [Array<String>]
834
851
  def merge_module_function_note_lines(indent, insertion, name, info)
835
- unless insertion.respond_to?(:module_function) && insertion.module_function && !info[:has_module_function_note]
836
- return []
837
- end
852
+ return [] unless insertion.respond_to?(:module_function) && insertion.module_function && !info[:has_module_function_note]
838
853
 
839
854
  included_vis = insertion.included_instance_visibility || :private
840
855
  ["#{indent}# @note module_function: defines ##{name} (visibility: #{included_vis})"]
@@ -1228,9 +1243,7 @@ module Docscribe
1228
1243
  end
1229
1244
 
1230
1245
  method_name = :"build_#{arg_node.type}_lines"
1231
- if respond_to?(method_name, true)
1232
- return send(method_name, arg_node, indent, external_sig, param_types_override, **opts)
1233
- end
1246
+ return send(method_name, arg_node, indent, external_sig, param_types_override, **opts) if respond_to?(method_name, true)
1234
1247
 
1235
1248
  []
1236
1249
  end
@@ -158,7 +158,8 @@ module Docscribe
158
158
  # @param [String] code the Ruby source code string to parse and rewrite
159
159
  # @param [Hash<Symbol, Object>] options hash containing :config, :file, and :core_rbs_provider
160
160
  # @raise [Docscribe::ParseError]
161
- # @return [Hash<Symbol, Docscribe::Config, String, Parser::Source::Buffer, Parser::AST::Node, Docscribe::Types::ProviderChain, nil, Object, nil>]
161
+ # @return [Hash<Symbol, Docscribe::Config, String, Parser::Source::Buffer, Parser::AST::Node, Docscribe::Types::ProviderChain, nil, Object, nil>] rewrite environment with keys
162
+ # :config, :file, :buffer, :ast, :core_rbs_provider
162
163
  def setup_rewrite_env(code, options)
163
164
  config = options[:config] || Docscribe::Config.load
164
165
  file = (options[:file] || '(inline)').to_s
@@ -454,7 +455,8 @@ module Docscribe
454
455
  # Plugin insertion priority
455
456
  #
456
457
  # @private
457
- # @param [Hash<Symbol, Object>, Docscribe::InlineRewriter::Collector::Insertion, Docscribe::InlineRewriter::Collector::AttrInsertion] insertion the collected method insertion
458
+ # @param [Hash<Symbol, Object>, Docscribe::InlineRewriter::Collector::Insertion, Docscribe::InlineRewriter::Collector::AttrInsertion] insertion the collected
459
+ # method insertion (plugin hash or collected insertion object)
458
460
  # @raise [StandardError]
459
461
  # @return [Integer]
460
462
  # @return [Integer] if StandardError
@@ -469,7 +471,8 @@ module Docscribe
469
471
  # Plugin insertion label
470
472
  #
471
473
  # @private
472
- # @param [Hash<Symbol, Object>, Docscribe::InlineRewriter::Collector::Insertion, Docscribe::InlineRewriter::Collector::AttrInsertion] insertion the collected method insertion
474
+ # @param [Hash<Symbol, Object>, Docscribe::InlineRewriter::Collector::Insertion, Docscribe::InlineRewriter::Collector::AttrInsertion] insertion the collected
475
+ # method insertion (plugin hash or collected insertion object)
473
476
  # @raise [StandardError]
474
477
  # @return [String]
475
478
  # @return [String] if StandardError
@@ -485,7 +488,8 @@ module Docscribe
485
488
  # Plugin insertion line
486
489
  #
487
490
  # @private
488
- # @param [Hash<Symbol, Object>, Docscribe::InlineRewriter::Collector::Insertion, Docscribe::InlineRewriter::Collector::AttrInsertion] insertion the collected method insertion
491
+ # @param [Hash<Symbol, Object>, Docscribe::InlineRewriter::Collector::Insertion, Docscribe::InlineRewriter::Collector::AttrInsertion] insertion the collected
492
+ # method insertion (plugin hash or collected insertion object)
489
493
  # @raise [StandardError]
490
494
  # @return [Integer, nil]
491
495
  # @return [nil] if StandardError
@@ -630,9 +634,7 @@ module Docscribe
630
634
  def normalize_plugin_doc(doc, indent, config:, anchor_node:)
631
635
  doc = normalize_plugin_doc_indent(doc, indent)
632
636
  doc = trim_trailing_blank_lines(doc)
633
- if anchor_node && %i[def defs].include?(anchor_node.type) && config.include_default_message?
634
- doc = prepend_default_message_if_no_prose(doc, anchor_node, indent, config)
635
- end
637
+ doc = prepend_default_message_if_no_prose(doc, anchor_node, indent, config) if anchor_node && %i[def defs].include?(anchor_node.type) && config.include_default_message?
636
638
  doc
637
639
  end
638
640
 
@@ -794,9 +796,7 @@ module Docscribe
794
796
  # @return [void]
795
797
  def merge_existing_descriptions!(params, parsed)
796
798
  params[:param_descriptions] = parsed[:param_descriptions] if parsed[:param_descriptions].any?
797
- if parsed[:return_description] && !parsed[:return_description].start_with?('if ')
798
- params[:return_description] = parsed[:return_description]
799
- end
799
+ params[:return_description] = parsed[:return_description] if parsed[:return_description] && !parsed[:return_description].start_with?('if ')
800
800
  params[:description] = parsed[:description] if parsed[:description].any?
801
801
  end
802
802
 
@@ -841,13 +841,27 @@ module Docscribe
841
841
  # @return [Docscribe::Types::MethodSignature, nil]
842
842
  def resolve_external_signature(insertion, signature_provider)
843
843
  node_name = SourceHelpers.node_name(insertion.node) #: Symbol
844
+ param_count, param_names = extract_param_info(insertion)
844
845
  signature_provider&.signature_for(
845
846
  container: insertion.container,
846
847
  scope: insertion.scope,
847
- name: node_name
848
+ name: node_name,
849
+ param_count: param_count,
850
+ param_names: param_names
848
851
  )
849
852
  end
850
853
 
854
+ # @private
855
+ # @param [Object] insertion
856
+ # @return [(Integer?, Array<Object>)]
857
+ def extract_param_info(insertion)
858
+ args = DocBuilder.extract_args_from_node(insertion.node)
859
+ empty = [] #: Array[untyped]
860
+ return [nil, empty] unless args
861
+
862
+ [args.children.length, args.children.map { |a| a.children.first.to_s if a.respond_to?(:children) }.compact]
863
+ end
864
+
851
865
  # Resolve param types
852
866
  #
853
867
  # @private
@@ -1079,7 +1093,8 @@ module Docscribe
1079
1093
  # @param [Docscribe::Config] config the active Docscribe::Config
1080
1094
  # @param [Docscribe::Types::ProviderChain, nil] signature_provider external RBS signature provider
1081
1095
  # @param [Parser::Source::Range] bol_range the beginning-of-line range for the attribute node
1082
- # @return [Hash<Symbol, Docscribe::InlineRewriter::Collector::AttrInsertion, Docscribe::Config, Docscribe::Types::ProviderChain, nil, Parser::Source::Range>]
1096
+ # @return [Hash<Symbol, Docscribe::InlineRewriter::Collector::AttrInsertion, Docscribe::Config, Docscribe::Types::ProviderChain, nil, Parser::Source::Range>] attr insertion params with keys
1097
+ # :insertion, :config, :signature_provider, :bol_range
1083
1098
  def attr_insertion_params(insertion, config, signature_provider, bol_range)
1084
1099
  {
1085
1100
  insertion: insertion, config: config,
@@ -1237,19 +1252,52 @@ module Docscribe
1237
1252
  # @return [String, nil]
1238
1253
  # @return [nil] if StandardError
1239
1254
  def build_attr_merge_additions(ins:, existing_lines:, config:, signature_provider:)
1240
- missing = missing_attr_names(ins, existing_lines)
1241
- return '' if missing.empty?
1242
-
1243
1255
  indent = SourceHelpers.line_indent(ins.node)
1244
- lines = [] #: Array[String]
1245
- lines << "#{indent}#" if existing_lines.any? && existing_lines.last.strip != '#'
1246
- lines.concat(build_attr_doc_lines(ins, indent: indent, config: config,
1247
- signature_provider: signature_provider, names: missing))
1248
- lines.map { |l| "#{l}\n" }.join
1256
+ lines = build_attr_merge_missing_lines(ins, existing_lines, indent, config, signature_provider)
1257
+ lines.concat(build_attr_merge_tag_lines(ins, existing_lines, indent, config, signature_provider))
1258
+
1259
+ lines.empty? ? '' : lines.map { |l| "#{l}\n" }.join
1249
1260
  rescue StandardError
1250
1261
  nil
1251
1262
  end
1252
1263
 
1264
+ # @private
1265
+ # @param [Object] ins
1266
+ # @param [Array<String>] existing_lines
1267
+ # @param [String] indent
1268
+ # @param [Docscribe::Config] config
1269
+ # @param [Docscribe::Types::ProviderChain, nil] signature_provider
1270
+ # @return [Array<String>]
1271
+ def build_attr_merge_missing_lines(ins, existing_lines, indent, config, signature_provider)
1272
+ missing = missing_attr_names(ins, existing_lines)
1273
+ lines = [] #: Array[String]
1274
+
1275
+ unless missing.empty?
1276
+ lines << "#{indent}#" if existing_lines.any? && existing_lines.last.strip != '#'
1277
+ lines.concat(build_attr_doc_lines(ins, indent: indent, config: config,
1278
+ signature_provider: signature_provider, names: missing))
1279
+ end
1280
+
1281
+ lines
1282
+ end
1283
+
1284
+ # @private
1285
+ # @param [Object] ins
1286
+ # @param [Array<String>] existing_lines
1287
+ # @param [String] indent
1288
+ # @param [Docscribe::Config] config
1289
+ # @param [Docscribe::Types::ProviderChain, nil] signature_provider
1290
+ # @return [Array<String>]
1291
+ def build_attr_merge_tag_lines(ins, existing_lines, indent, config, signature_provider)
1292
+ tag_additions = build_missing_tag_additions(ins, existing_lines, indent, config, signature_provider)
1293
+ return [] if tag_additions.empty?
1294
+
1295
+ result = [] #: Array[String]
1296
+ result << "#{indent}#" if existing_lines.any? && existing_lines.last.strip != '#'
1297
+ result.concat(tag_additions)
1298
+ result
1299
+ end
1300
+
1253
1301
  # Missing attr names
1254
1302
  #
1255
1303
  # @private
@@ -1278,6 +1326,80 @@ module Docscribe
1278
1326
  names
1279
1327
  end
1280
1328
 
1329
+ # Build missing tag additions for existing @!attribute lines
1330
+ #
1331
+ # @private
1332
+ # @param [Docscribe::InlineRewriter::Collector::AttrInsertion] ins the attribute insertion object
1333
+ # @param [Array<String>] existing_lines array of existing doc comment lines
1334
+ # @param [String] indent whitespace indentation prefix
1335
+ # @param [Docscribe::Config] config the active Docscribe::Config
1336
+ # @param [Docscribe::Types::ProviderChain, nil] signature_provider external RBS signature provider
1337
+ # @return [Array<String>]
1338
+ def build_missing_tag_additions(ins, existing_lines, indent, config, signature_provider)
1339
+ ins.names.each_with_object([]) do |name_sym, additions|
1340
+ idx = existing_attr_line_index(existing_lines, name_sym.to_s)
1341
+ next unless idx
1342
+
1343
+ attr_type = attribute_type(ins, name_sym, config, signature_provider: signature_provider)
1344
+ additions << "#{indent}# @return [#{attr_type}]" if attr_return_missing?(ins.access, existing_lines, idx)
1345
+ additions << format_attribute_param_tag(indent, 'value', attr_type, style: config.param_tag_style) if attr_param_missing?(ins.access, existing_lines, idx)
1346
+ end
1347
+ end
1348
+
1349
+ # @private
1350
+ # @param [Symbol] access
1351
+ # @param [Array<String>] existing_lines
1352
+ # @param [Integer] idx
1353
+ # @return [Boolean]
1354
+ def attr_return_missing?(access, existing_lines, idx)
1355
+ %i[r rw].include?(access) && !existing_lines_contain_tag?(existing_lines, idx, 'return')
1356
+ end
1357
+
1358
+ # @private
1359
+ # @param [Symbol] access
1360
+ # @param [Array<String>] existing_lines
1361
+ # @param [Integer] idx
1362
+ # @return [Boolean]
1363
+ def attr_param_missing?(access, existing_lines, idx)
1364
+ %i[w rw].include?(access) && !existing_lines_contain_tag?(existing_lines, idx, 'param')
1365
+ end
1366
+
1367
+ # Find index of line containing @!attribute for a specific name
1368
+ #
1369
+ # @private
1370
+ # @param [Array<String>] lines array of existing doc comment lines
1371
+ # @param [String] name the attribute name to search for
1372
+ # @return [Integer?]
1373
+ def existing_attr_line_index(lines, name)
1374
+ Array(lines).each_with_index do |line, idx|
1375
+ return idx if line.match?(/\A\s*#\s*@!attribute\b(?:\s+\[[^\]]+\])?\s+#{Regexp.escape(name)}\b/)
1376
+ end
1377
+ nil
1378
+ end
1379
+
1380
+ # Check if lines after an @!attribute line contain a specific tag
1381
+ #
1382
+ # Walks forward from attr_line_idx + 1, stopping at the next top-level
1383
+ # directive or non-comment line.
1384
+ #
1385
+ # @private
1386
+ # @param [Array<String>] lines array of existing doc comment lines
1387
+ # @param [Integer] attr_line_idx index of the @!attribute line
1388
+ # @param [String] tag_name the tag name to search for (e.g. "return", "param")
1389
+ # @return [Boolean]
1390
+ def existing_lines_contain_tag?(lines, attr_line_idx, tag_name)
1391
+ i = attr_line_idx + 1
1392
+ while i < lines.length
1393
+ line = lines[i]
1394
+ break unless line.match?(/\A\s*#/) # not a comment line
1395
+ break if line.match?(/\A\s*# @(?!@)/) # next top-level directive (single space)
1396
+ return true if line.match?(/\A\s*#\s{2,}@#{tag_name}\b/)
1397
+
1398
+ i += 1
1399
+ end
1400
+ false
1401
+ end
1402
+
1281
1403
  # Attribute allowed
1282
1404
  #
1283
1405
  # @private
@@ -651,7 +651,8 @@ module Docscribe
651
651
  def rewrite_and_cache(file, strategy, config, key, mtime)
652
652
  src = File.read(file)
653
653
  rbs = config.respond_to?(:core_rbs_provider) ? config.core_rbs_provider : nil
654
- result = Docscribe::InlineRewriter.rewrite_with_report(src, strategy: strategy, config: config, core_rbs_provider: rbs, file: file)
654
+ result = Docscribe::InlineRewriter.rewrite_with_report(src, strategy: strategy, config: config,
655
+ core_rbs_provider: rbs, file: file)
655
656
  @file_cache[key] = { mtime: mtime, src: src, result: result }
656
657
  [src, result]
657
658
  end
@@ -689,7 +690,7 @@ module Docscribe
689
690
  # @private
690
691
  # @param [UNIXSocket] client connected client socket
691
692
  # @param [String, Integer] id request ID
692
- # @param [Object] result result data
693
+ # @param [Hash<String, Object>] result result data
693
694
  # @return [void]
694
695
  def send_result(client, id, result)
695
696
  response = { jsonrpc: '2.0', id: id, result: result }
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docscribe
4
+ module Types
5
+ # Selects best matching overload from RBS signatures for given arguments.
6
+ module OverloadSelector
7
+ class << self
8
+ # @param [Array<Object>] overloads
9
+ # @param [Integer] arg_count
10
+ # @param [Array<String>] param_names
11
+ # @return [Object?]
12
+ def select(overloads, arg_count:, param_names: [])
13
+ return nil if overloads.nil? || overloads.empty?
14
+ return overloads.first if overloads.size == 1
15
+
16
+ best = best_match(overloads, arg_count, param_names)
17
+ best&.first || overloads.first
18
+ end
19
+
20
+ # @param [Array<Object>] overloads
21
+ # @param [Integer] arg_count
22
+ # @param [Array<String>] param_names
23
+ # @return [(Object, Integer)?]
24
+ def best_match(overloads, arg_count, param_names)
25
+ candidates = overloads.map { |sig| score_signature(sig, arg_count: arg_count, param_names: param_names) }
26
+ candidates.compact.max_by { |_sig, score| score }
27
+ end
28
+
29
+ private
30
+
31
+ # @private
32
+ # @param [Object] sig
33
+ # @param [Integer] arg_count
34
+ # @param [Array<String>] param_names
35
+ # @return [(Object, Integer)?]
36
+ def score_signature(sig, arg_count:, param_names:)
37
+ score = 0
38
+
39
+ pos_count = sig.positional_types&.length.to_i
40
+ return nil if pos_count > arg_count
41
+
42
+ score += score_positional(pos_count, arg_count, sig)
43
+ score += score_params(sig, param_names)
44
+ score += 3 if sig.return_type && !sig.return_type.empty?
45
+ score += 1 if sig.return_type && sig.return_type != 'Object'
46
+
47
+ [sig, score]
48
+ end
49
+
50
+ # @private
51
+ # @param [Integer] pos_count
52
+ # @param [Integer] arg_count
53
+ # @param [Object] sig
54
+ # @return [Integer]
55
+ def score_positional(pos_count, arg_count, sig)
56
+ if pos_count == arg_count
57
+ 10
58
+ elsif pos_count < arg_count && sig.rest_positional
59
+ 5
60
+ else
61
+ 0
62
+ end
63
+ end
64
+
65
+ # @private
66
+ # @param [Object] sig
67
+ # @param [Array<String>] param_names
68
+ # @return [Integer]
69
+ def score_params(sig, param_names)
70
+ if sig.param_types
71
+ (sig.param_types.keys & param_names).length * 2
72
+ else
73
+ 0
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'overload_selector'
4
+
3
5
  module Docscribe
4
6
  module Types
5
7
  # Resolve method signatures by querying a list of providers in order.
@@ -22,18 +24,39 @@ module Docscribe
22
24
 
23
25
  # Resolve a method signature from the first provider that can supply it.
24
26
  #
27
+ # When overloads are present, selects the best-matching signature.
28
+ #
25
29
  # @param [String] container e.g. "MyModule::MyClass"
26
30
  # @param [Symbol] scope :instance or :class
27
31
  # @param [Symbol, String] name method name
32
+ # @param [Integer?] param_count number of actual arguments
33
+ # @param [Array<String>] param_names actual parameter names
28
34
  # @return [Docscribe::Types::MethodSignature, nil]
29
- def signature_for(container:, scope:, name:)
35
+ def signature_for(container:, scope:, name:, param_count: nil, param_names: [])
30
36
  @providers.each do |provider|
31
37
  sig = provider.signature_for(container: container, scope: scope, name: name)
32
- return sig if sig
38
+ next unless sig
39
+
40
+ return sig unless sig.overloads&.any?
41
+
42
+ best = select_overload(sig, param_count, param_names)
43
+ return best if best
33
44
  end
34
45
 
35
46
  nil
36
47
  end
48
+
49
+ # @param [Docscribe::Types::MethodSignature] sig
50
+ # @param [Integer?] param_count
51
+ # @param [Array<String>] param_names
52
+ # @return [Docscribe::Types::MethodSignature, nil]
53
+ def select_overload(sig, param_count, param_names)
54
+ OverloadSelector.select(
55
+ [sig, *sig.overloads],
56
+ arg_count: param_count || 0,
57
+ param_names: param_names
58
+ )
59
+ end
37
60
  end
38
61
  end
39
62
  end
@@ -21,7 +21,12 @@ module Docscribe
21
21
  # @!attribute [rw] rest_keywords
22
22
  # @return [Docscribe::Types::RestKeywords, nil]
23
23
  # @param [Docscribe::Types::RestKeywords, nil] value
24
+ #
25
+ # @!attribute [rw] overloads
26
+ # @return [Array<Docscribe::Types::MethodSignature>, nil]
27
+ # @param [Array<Docscribe::Types::MethodSignature>, nil] value
24
28
  MethodSignature = Struct.new(:return_type, :param_types, :positional_types, :rest_positional, :rest_keywords,
29
+ :overloads,
25
30
  keyword_init: true)
26
31
 
27
32
  # @!attribute [rw] name
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docscribe
4
- VERSION = '1.5.2'
4
+ VERSION = '1.6.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docscribe
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.2
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - unurgunite
@@ -163,6 +163,8 @@ files:
163
163
  - lib/docscribe/cli.rb
164
164
  - lib/docscribe/cli/check_for_comments.rb
165
165
  - lib/docscribe/cli/config_builder.rb
166
+ - lib/docscribe/cli/config_dump.rb
167
+ - lib/docscribe/cli/coverage.rb
166
168
  - lib/docscribe/cli/formatters.rb
167
169
  - lib/docscribe/cli/formatters/json.rb
168
170
  - lib/docscribe/cli/formatters/sarif.rb
@@ -188,6 +190,7 @@ files:
188
190
  - lib/docscribe/config/utils.rb
189
191
  - lib/docscribe/infer.rb
190
192
  - lib/docscribe/infer/ast_walk.rb
193
+ - lib/docscribe/infer/behavior.rb
191
194
  - lib/docscribe/infer/constants.rb
192
195
  - lib/docscribe/infer/literals.rb
193
196
  - lib/docscribe/infer/names.rb
@@ -209,6 +212,7 @@ files:
209
212
  - lib/docscribe/plugin/registry.rb
210
213
  - lib/docscribe/plugin/tag.rb
211
214
  - lib/docscribe/server.rb
215
+ - lib/docscribe/types/overload_selector.rb
212
216
  - lib/docscribe/types/provider_chain.rb
213
217
  - lib/docscribe/types/rbs/collection_loader.rb
214
218
  - lib/docscribe/types/rbs/provider.rb
@@ -230,7 +234,7 @@ metadata:
230
234
  changelog_uri: https://github.com/unurgunite/docscribe/blob/master/CHANGELOG.md
231
235
  rubygems_mfa_required: 'true'
232
236
  post_install_message: |
233
- You installed docscribe 1.5.2. Your future self (and your team) thank you.
237
+ You installed docscribe 1.6.0. Your future self (and your team) thank you.
234
238
 
235
239
  $ docscribe --help
236
240