docscribe 1.4.1 → 1.4.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +149 -0
  3. data/lib/docscribe/cli/config_builder.rb +125 -35
  4. data/lib/docscribe/cli/generate.rb +288 -117
  5. data/lib/docscribe/cli/init.rb +49 -13
  6. data/lib/docscribe/cli/options.rb +302 -127
  7. data/lib/docscribe/cli/run.rb +391 -135
  8. data/lib/docscribe/cli.rb +23 -5
  9. data/lib/docscribe/config/defaults.rb +11 -11
  10. data/lib/docscribe/config/emit.rb +1 -0
  11. data/lib/docscribe/config/filtering.rb +24 -11
  12. data/lib/docscribe/config/loader.rb +7 -4
  13. data/lib/docscribe/config/plugin.rb +1 -0
  14. data/lib/docscribe/config/rbs.rb +31 -22
  15. data/lib/docscribe/config/sorbet.rb +41 -15
  16. data/lib/docscribe/config/sorting.rb +1 -0
  17. data/lib/docscribe/config/template.rb +1 -0
  18. data/lib/docscribe/config/utils.rb +1 -0
  19. data/lib/docscribe/config.rb +1 -0
  20. data/lib/docscribe/infer/constants.rb +15 -0
  21. data/lib/docscribe/infer/literals.rb +43 -25
  22. data/lib/docscribe/infer/names.rb +24 -15
  23. data/lib/docscribe/infer/params.rb +52 -6
  24. data/lib/docscribe/infer/raises.rb +24 -14
  25. data/lib/docscribe/infer/returns.rb +365 -182
  26. data/lib/docscribe/infer.rb +10 -9
  27. data/lib/docscribe/inline_rewriter/collector.rb +766 -375
  28. data/lib/docscribe/inline_rewriter/doc_block.rb +217 -74
  29. data/lib/docscribe/inline_rewriter/doc_builder.rb +1488 -602
  30. data/lib/docscribe/inline_rewriter/source_helpers.rb +100 -52
  31. data/lib/docscribe/inline_rewriter/tag_sorter.rb +109 -48
  32. data/lib/docscribe/inline_rewriter.rb +1009 -595
  33. data/lib/docscribe/plugin/base/collector_plugin.rb +2 -3
  34. data/lib/docscribe/plugin/base/tag_plugin.rb +1 -1
  35. data/lib/docscribe/plugin/registry.rb +34 -7
  36. data/lib/docscribe/plugin.rb +48 -17
  37. data/lib/docscribe/types/rbs/collection_loader.rb +0 -1
  38. data/lib/docscribe/types/rbs/provider.rb +75 -26
  39. data/lib/docscribe/types/rbs/type_formatter.rb +127 -59
  40. data/lib/docscribe/types/sorbet/base_provider.rb +31 -12
  41. data/lib/docscribe/version.rb +1 -1
  42. metadata +2 -2
data/lib/docscribe/cli.rb CHANGED
@@ -6,6 +6,7 @@ require 'docscribe/cli/options'
6
6
  require 'docscribe/cli/run'
7
7
 
8
8
  module Docscribe
9
+ # CLI entry point and command dispatch.
9
10
  module CLI
10
11
  class << self
11
12
  # Main CLI entry point.
@@ -19,18 +20,35 @@ module Docscribe
19
20
  # @return [Integer] process exit code
20
21
  def run(argv)
21
22
  argv = argv.dup
23
+ return dispatch_subcommand(argv) if subcommand?(argv.first)
22
24
 
25
+ options = Docscribe::CLI::Options.parse!(argv)
26
+ Docscribe::CLI::Run.run(options: options, argv: argv)
27
+ end
28
+
29
+ private
30
+
31
+ # @private
32
+ # @param [String] cmd
33
+ # @return [Boolean]
34
+ def subcommand?(cmd)
35
+ %w[init generate].include?(cmd)
36
+ end
37
+
38
+ # @private
39
+ # @param [Array<String>] argv
40
+ # @return [Integer, nil]
41
+ def dispatch_subcommand(argv)
23
42
  case argv.first
24
43
  when 'init'
25
44
  argv.shift
26
- return Docscribe::CLI::Init.run(argv)
45
+ Docscribe::CLI::Init.run(argv)
27
46
  when 'generate'
28
47
  argv.shift
29
- return Docscribe::CLI::Generate.run(argv)
48
+ Docscribe::CLI::Generate.run(argv)
49
+ else
50
+ 0
30
51
  end
31
-
32
- options = Docscribe::CLI::Options.parse!(argv)
33
- Docscribe::CLI::Run.run(options: options, argv: argv)
34
52
  end
35
53
  end
36
54
  end
@@ -33,14 +33,14 @@ module Docscribe
33
33
  },
34
34
  'methods' => {
35
35
  'instance' => {
36
- 'public' => {},
37
- 'protected' => {},
38
- 'private' => {}
36
+ 'public' => {}, #: Hash[String, untyped]
37
+ 'protected' => {}, #: Hash[String, untyped]
38
+ 'private' => {} #: Hash[String, untyped]
39
39
  },
40
40
  'class' => {
41
- 'public' => {},
42
- 'protected' => {},
43
- 'private' => {}
41
+ 'public' => {}, #: Hash[String, untyped]
42
+ 'protected' => {}, #: Hash[String, untyped]
43
+ 'private' => {} #: Hash[String, untyped]
44
44
  }
45
45
  },
46
46
  'inference' => {
@@ -51,10 +51,10 @@ module Docscribe
51
51
  'filter' => {
52
52
  'visibilities' => %w[public protected private],
53
53
  'scopes' => %w[instance class],
54
- 'include' => [],
55
- 'exclude' => [],
54
+ 'include' => [], #: Array[String]
55
+ 'exclude' => [], #: Array[String]
56
56
  'files' => {
57
- 'include' => [],
57
+ 'include' => [], #: Array[String]
58
58
  'exclude' => ['spec']
59
59
  }
60
60
  },
@@ -62,7 +62,7 @@ module Docscribe
62
62
  'enabled' => false,
63
63
  'collection' => false,
64
64
  'sig_dirs' => ['sig'],
65
- 'collection_dirs' => [],
65
+ 'collection_dirs' => [], #: Array[String]
66
66
  'collapse_generics' => false
67
67
  },
68
68
  'sorbet' => {
@@ -71,7 +71,7 @@ module Docscribe
71
71
  'collapse_generics' => false
72
72
  },
73
73
  'plugins' => {
74
- 'require' => []
74
+ 'require' => [] #: Array[String]
75
75
  }
76
76
  }.freeze
77
77
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docscribe
4
+ # Emit-related configuration (headers, visibility tags, etc.).
4
5
  class Config
5
6
  # Whether to emit method header lines such as:
6
7
  # # +MyClass#foo+ -> Integer
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docscribe
4
+ # File and method include/exclude filtering.
4
5
  class Config
5
6
  # Decide whether a file path should be processed based on `filter.files`.
6
7
  #
@@ -11,15 +12,8 @@ module Docscribe
11
12
  # @raise [StandardError]
12
13
  # @return [Boolean]
13
14
  def process_file?(path)
14
- files = raw.dig('filter', 'files') || {}
15
- include_patterns = normalize_file_patterns(files['include'])
16
- exclude_patterns = normalize_file_patterns(files['exclude'])
17
-
18
- rel = begin
19
- Pathname.new(path).expand_path.relative_path_from(Pathname.pwd).cleanpath.to_s
20
- rescue StandardError
21
- path
22
- end
15
+ include_patterns, exclude_patterns = load_file_patterns
16
+ rel = relative_path(path)
23
17
 
24
18
  return false if file_matches_any?(exclude_patterns, rel)
25
19
  return true if include_patterns.empty?
@@ -27,6 +21,27 @@ module Docscribe
27
21
  file_matches_any?(include_patterns, rel)
28
22
  end
29
23
 
24
+ # Load normalized file include/exclude patterns from config.
25
+ #
26
+ # @private
27
+ # @return [Array(Array<String>, Array<String>)] include_patterns, exclude_patterns
28
+ def load_file_patterns
29
+ files = raw.dig('filter', 'files') || {}
30
+ [normalize_file_patterns(files['include']), normalize_file_patterns(files['exclude'])]
31
+ end
32
+
33
+ # Compute the relative path for filtering.
34
+ #
35
+ # @private
36
+ # @param [String] path
37
+ # @raise [StandardError]
38
+ # @return [String]
39
+ def relative_path(path)
40
+ Pathname.new(path).expand_path.relative_path_from(Pathname.pwd).cleanpath.to_s
41
+ rescue StandardError
42
+ path
43
+ end
44
+
30
45
  # Decide whether a method should be processed based on configured method filters.
31
46
  #
32
47
  # Method IDs are normalized as:
@@ -55,8 +70,6 @@ module Docscribe
55
70
  matches_any?(inc, method_id)
56
71
  end
57
72
 
58
- private
59
-
60
73
  # Normalize file filter patterns:
61
74
  # - compact nils
62
75
  # - stringify
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docscribe
4
+ # YAML config file loading and resolution.
4
5
  class Config
5
6
  # Load Docscribe configuration from YAML.
6
7
  #
@@ -12,7 +13,7 @@ module Docscribe
12
13
  # @param [String, nil] path optional config path
13
14
  # @return [Docscribe::Config]
14
15
  def self.load(path = nil)
15
- raw = {}
16
+ raw = {} #: Hash[String, untyped]
16
17
  if path && File.file?(path)
17
18
  raw = safe_load_file_compat(path)
18
19
  elsif File.file?('docscribe.yml')
@@ -30,10 +31,12 @@ module Docscribe
30
31
  # @return [Hash]
31
32
  def self.safe_load_file_compat(path)
32
33
  if YAML.respond_to?(:safe_load_file)
33
- YAML.safe_load_file(path, permitted_classes: [], permitted_symbols: [], aliases: true) || {}
34
+ YAML.safe_load_file(path,
35
+ permitted_classes: [], permitted_symbols: [],
36
+ aliases: true) || {} #: Hash[String, untyped]
34
37
  else
35
38
  yaml = File.open(path, 'r:bom|utf-8', &:read)
36
- safe_load_compat(yaml, filename: path) || {}
39
+ safe_load_compat(yaml, filename: path) || {} #: Hash[String, untyped]
37
40
  end
38
41
  end
39
42
 
@@ -50,7 +53,7 @@ module Docscribe
50
53
  permitted_symbols: [],
51
54
  aliases: true,
52
55
  filename: filename
53
- )
56
+ ) #: Hash[String, untyped]
54
57
  rescue ArgumentError
55
58
  # Older Psych signature uses positional args
56
59
  Psych.safe_load(yaml, [], [], true, filename)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docscribe
4
+ # Plugin loading and registration from config.
4
5
  class Config
5
6
  # Load and register plugins declared under `plugins.require` in config.
6
7
  #
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docscribe
4
+ # RBS signature provider configuration.
4
5
  class Config
5
6
  # Return a memoized RBS provider if RBS integration is enabled and available.
6
7
  #
@@ -13,16 +14,7 @@ module Docscribe
13
14
  return nil unless rbs_enabled?
14
15
  return nil unless ruby_supports_rbs?
15
16
 
16
- @rbs_provider ||= begin
17
- require 'docscribe/types/rbs/provider'
18
- Docscribe::Types::RBS::Provider.new(
19
- sig_dirs: rbs_sig_dirs,
20
- collection_dirs: rbs_collection_dirs,
21
- collapse_generics: rbs_collapse_generics?
22
- )
23
- rescue LoadError
24
- nil
25
- end
17
+ @rbs_provider ||= build_rbs_provider
26
18
  end
27
19
 
28
20
  # Whether RBS integration is enabled.
@@ -32,27 +24,17 @@ module Docscribe
32
24
  fetch_bool(%w[rbs enabled], false)
33
25
  end
34
26
 
35
- # Method documentation.
36
- #
37
27
  # @raise [LoadError]
38
28
  # @return [Object]
39
29
  def core_rbs_provider
40
30
  return nil unless ruby_supports_rbs?
41
31
 
42
- @core_rbs_provider ||= begin
43
- require 'docscribe/types/rbs/provider'
44
- Docscribe::Types::RBS::Provider.new(
45
- sig_dirs: [],
46
- collapse_generics: false
47
- )
48
- rescue LoadError
49
- nil
50
- end
32
+ @core_rbs_provider ||= build_core_rbs_provider
51
33
  end
52
34
 
53
35
  private
54
36
 
55
- # Method documentation.
37
+ # Check whether the current Ruby version supports RBS (requires 3.0+).
56
38
  #
57
39
  # @private
58
40
  # @return [Boolean]
@@ -66,6 +48,33 @@ module Docscribe
66
48
  false
67
49
  end
68
50
 
51
+ # @private
52
+ # @raise [LoadError]
53
+ # @return [Docscribe::Types::RBS::Provider, nil]
54
+ def build_rbs_provider
55
+ require 'docscribe/types/rbs/provider'
56
+ Docscribe::Types::RBS::Provider.new(
57
+ sig_dirs: rbs_sig_dirs,
58
+ collection_dirs: rbs_collection_dirs,
59
+ collapse_generics: rbs_collapse_generics?
60
+ )
61
+ rescue LoadError
62
+ nil
63
+ end
64
+
65
+ # @private
66
+ # @raise [LoadError]
67
+ # @return [Docscribe::Types::RBS::Provider, nil]
68
+ def build_core_rbs_provider
69
+ require 'docscribe/types/rbs/provider'
70
+ Docscribe::Types::RBS::Provider.new(
71
+ sig_dirs: [],
72
+ collapse_generics: false
73
+ )
74
+ rescue LoadError
75
+ nil
76
+ end
77
+
69
78
  # Signature directories used by the RBS provider.
70
79
  #
71
80
  # @private
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docscribe
4
+ # Sorbet and signature provider configuration.
4
5
  class Config
5
6
  # Build the effective external signature provider chain for a given source.
6
7
  #
@@ -16,25 +17,50 @@ module Docscribe
16
17
  # @raise [LoadError]
17
18
  # @return [Docscribe::Types::ProviderChain, nil]
18
19
  def signature_provider_for(source:, file:)
19
- providers = []
20
+ providers = [] #: Array[untyped]
21
+ append_sorbet_providers(providers, source: source, file: file)
22
+ providers << rbs_provider if rbs_enabled?
23
+ build_provider_chain(providers)
24
+ end
20
25
 
21
- if sorbet_enabled?
22
- begin
23
- require 'docscribe/types/sorbet/source_provider'
24
- providers << Docscribe::Types::Sorbet::SourceProvider.new(
25
- source: source,
26
- file: file,
27
- collapse_generics: sorbet_collapse_generics?
28
- )
29
- rescue LoadError
30
- # Sorbet support is optional; fall back quietly.
31
- end
26
+ # Append Sorbet-based providers to the list.
27
+ #
28
+ # @private
29
+ # @param [Array] providers
30
+ # @param [String] source
31
+ # @param [String] file
32
+ # @return [void]
33
+ def append_sorbet_providers(providers, source:, file:)
34
+ return unless sorbet_enabled?
32
35
 
33
- providers << sorbet_rbi_provider
34
- end
36
+ providers << sorbet_source_provider(source, file)
37
+ providers << sorbet_rbi_provider
38
+ end
35
39
 
36
- providers << rbs_provider if rbs_enabled?
40
+ # Build a Sorbet source provider (inline sigs).
41
+ #
42
+ # @private
43
+ # @param [String] source
44
+ # @param [String] file
45
+ # @raise [LoadError]
46
+ # @return [Docscribe::Types::Sorbet::SourceProvider, nil]
47
+ def sorbet_source_provider(source, file)
48
+ require 'docscribe/types/sorbet/source_provider'
49
+ Docscribe::Types::Sorbet::SourceProvider.new(
50
+ source: source,
51
+ file: file,
52
+ collapse_generics: sorbet_collapse_generics?
53
+ )
54
+ rescue LoadError
55
+ nil
56
+ end
37
57
 
58
+ # Build the provider chain from a non-empty list, or return nil.
59
+ #
60
+ # @private
61
+ # @param [Array] providers
62
+ # @return [Docscribe::Types::ProviderChain, nil]
63
+ def build_provider_chain(providers)
38
64
  providers = providers.compact
39
65
  return nil if providers.empty?
40
66
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docscribe
4
+ # Tag sorting configuration.
4
5
  class Config
5
6
  # Whether sortable tag normalization is enabled for doc-like blocks.
6
7
  #
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docscribe
4
+ # Default YAML config template for `docscribe init`.
4
5
  class Config
5
6
  # Return the default YAML template used by `docscribe init`.
6
7
  #
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docscribe
4
+ # Internal config utility methods.
4
5
  class Config
5
6
  private
6
7
 
@@ -5,6 +5,7 @@ require 'pathname'
5
5
  require 'psych'
6
6
 
7
7
  module Docscribe
8
+ # Application configuration with deep-merge defaults and overrides.
8
9
  class Config
9
10
  # Raw config hash after deep-merging user config with defaults.
10
11
  #
@@ -7,5 +7,20 @@ module Docscribe
7
7
 
8
8
  # Ruby's implicit rescue target for bare `rescue`.
9
9
  DEFAULT_ERROR = 'StandardError'
10
+
11
+ # Node type to literal type name mapping.
12
+ LITERAL_TYPE_MAP = {
13
+ int: 'Integer',
14
+ float: 'Float',
15
+ str: 'String',
16
+ dstr: 'String',
17
+ sym: 'Symbol',
18
+ true: 'Boolean',
19
+ false: 'Boolean',
20
+ nil: 'nil',
21
+ array: 'Array',
22
+ hash: 'Hash',
23
+ regexp: 'Regexp'
24
+ }.freeze
10
25
  end
11
26
  end
@@ -24,31 +24,49 @@ module Docscribe
24
24
  def type_from_literal(node, fallback_type: FALLBACK_TYPE)
25
25
  return fallback_type unless node
26
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
27
+ literal_type_for(node.type) || const_type_for(node, fallback_type) ||
28
+ send_new_type_for(node, fallback_type) || fallback_type
29
+ end
30
+
31
+ # Map a node type symbol to a known literal type name.
32
+ #
33
+ # @note module_function: when included, also defines # (instance visibility: private)
34
+ # @private
35
+ # @param [Symbol] type node type
36
+ # @return [String, nil]
37
+ def literal_type_for(type)
38
+ LITERAL_TYPE_MAP[type]
39
+ end
40
+
41
+ # Extract a constant name from a `:const` node.
42
+ #
43
+ # @note module_function: when included, also defines # (instance visibility: private)
44
+ # @private
45
+ # @param [Parser::AST::Node] node
46
+ # @param [String] fallback_type
47
+ # @param [String] _fallback_type fallback type string (unused here)
48
+ # @return [String, nil]
49
+ def const_type_for(node, _fallback_type)
50
+ return unless node.type == :const
51
+
52
+ node.children.last.to_s
53
+ end
54
+
55
+ # Extract a type from a `Foo.new` send node.
56
+ #
57
+ # @note module_function: when included, also defines # (instance visibility: private)
58
+ # @private
59
+ # @param [Parser::AST::Node] node
60
+ # @param [String] fallback_type
61
+ # @param [String] _fallback_type fallback type string (unused here)
62
+ # @return [String, nil]
63
+ def send_new_type_for(node, _fallback_type)
64
+ return unless node.type == :send
65
+
66
+ recv, meth, = node.children
67
+ return unless meth == :new && recv&.type == :const
68
+
69
+ recv.children.last.to_s
52
70
  end
53
71
  end
54
72
  end
@@ -16,26 +16,35 @@ module Docscribe
16
16
  # Returns nil for unsupported nodes.
17
17
  #
18
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
19
+ # @param [Parser::AST::Node, nil] node constant-like AST node
20
20
  # @return [String, nil]
21
- def const_full_name(n)
22
- return nil unless n.is_a?(Parser::AST::Node)
21
+ def const_full_name(node)
22
+ return nil unless node.is_a?(Parser::AST::Node)
23
23
 
24
- case n.type
24
+ case node.type
25
25
  when :const
26
- scope, name = *n
27
- scope_name = const_full_name(scope)
26
+ build_const_full_name(node)
27
+ when :cbase
28
+ ''
29
+ end
30
+ end
28
31
 
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
32
+ # Build the fully qualified name from a `:const` node.
33
+ #
34
+ # @note module_function: when included, also defines # (instance visibility: private)
35
+ # @private
36
+ # @param [Parser::AST::Node] node a `:const` node
37
+ # @return [String]
38
+ def build_const_full_name(node)
39
+ scope, name = *node
40
+ scope_name = const_full_name(scope)
36
41
 
37
- when :cbase
38
- '' # represents leading :: scope
42
+ if scope_name && !scope_name.empty?
43
+ "#{scope_name}::#{name}"
44
+ elsif scope_name == ''
45
+ "::#{name}"
46
+ else
47
+ name.to_s
39
48
  end
40
49
  end
41
50
  end
@@ -23,23 +23,69 @@ module Docscribe
23
23
  # be treated specially as Hash
24
24
  # @return [String]
25
25
  def infer_param_type(name, default_str, fallback_type: FALLBACK_TYPE, treat_options_keyword_as_hash: true)
26
+ prefix_param_type(name) || inferred_param_type(name, default_str, fallback_type,
27
+ treat_options_keyword_as_hash: treat_options_keyword_as_hash)
28
+ end
29
+
30
+ # Return type for special parameter prefixes.
31
+ #
32
+ # @note module_function: when included, also defines # (instance visibility: private)
33
+ # @private
34
+ # @param [String] name parameter name
35
+ # @return [String, nil]
36
+ def prefix_param_type(name)
26
37
  return 'Array' if name.start_with?('*') && !name.start_with?('**')
27
38
  return 'Hash' if name.start_with?('**')
28
39
  return 'Proc' if name.start_with?('&')
29
40
 
30
- is_kw = name.end_with?(':')
31
- node = parse_expr(default_str)
32
- ty = Literals.type_from_literal(node, fallback_type: fallback_type)
41
+ nil
42
+ end
33
43
 
34
- if is_kw && default_str.nil?
35
- return (treat_options_keyword_as_hash && name == 'options:' ? 'Hash' : fallback_type)
44
+ # Infer type for a regular or keyword parameter with optional default.
45
+ #
46
+ # @note module_function: when included, also defines # (instance visibility: private)
47
+ # @private
48
+ # @param [String] name parameter name
49
+ # @param [String, nil] default_str default expression source
50
+ # @param [String] fallback_type
51
+ # @param [Boolean] treat_options_keyword_as_hash
52
+ # @return [String]
53
+ def inferred_param_type(name, default_str, fallback_type, treat_options_keyword_as_hash:)
54
+ if name.end_with?(':') && default_str.nil?
55
+ return options_keyword_type(name, treat_options_keyword_as_hash, fallback_type)
36
56
  end
37
57
 
38
- return 'Hash' if treat_options_keyword_as_hash && name == 'options:' && (default_str == '{}' || ty == 'Hash')
58
+ node = parse_expr(default_str)
59
+ ty = Literals.type_from_literal(node, fallback_type: fallback_type)
60
+
61
+ return 'Hash' if options_hash_keyword?(name, default_str, ty, treat_options_keyword_as_hash)
39
62
 
40
63
  ty
41
64
  end
42
65
 
66
+ # Return 'Hash' for a keyword parameter named 'options:' when special-cased, else fallback.
67
+ #
68
+ # @note module_function: when included, also defines #options_keyword_type (instance visibility: private)
69
+ # @param [String] name parameter name
70
+ # @param [Boolean] treat_options_keyword_as_hash whether to treat 'options:' as Hash
71
+ # @param [String] fallback_type type returned when not special-cased
72
+ # @return [String]
73
+ def options_keyword_type(name, treat_options_keyword_as_hash, fallback_type)
74
+ treat_options_keyword_as_hash && name == 'options:' ? 'Hash' : fallback_type
75
+ end
76
+
77
+ # Whether a keyword parameter named 'options:' with a hash default should be typed as Hash.
78
+ #
79
+ # @note module_function: when included, also defines #options_hash_keyword? (instance visibility: private)
80
+ # @param [String] name parameter name
81
+ # @param [String, nil] default_str default expression source
82
+ # @param [String] type inferred type
83
+ # @param [Boolean] treat_options_keyword_as_hash whether to treat 'options:' as Hash
84
+ # @return [Boolean]
85
+ def options_hash_keyword?(name, default_str, type, treat_options_keyword_as_hash)
86
+ treat_options_keyword_as_hash && name == 'options:' && (default_str == '{}' || type == 'Hash')
87
+ end
88
+
43
89
  # Parse a standalone expression for parameter-default inference.
44
90
  #
45
91
  # Returns nil if the expression is empty or cannot be parsed.