rigortype 0.1.3 → 0.1.4

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +125 -31
  3. data/lib/rigor/analysis/check_rules.rb +10 -18
  4. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
  5. data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
  9. data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
  10. data/lib/rigor/analysis/diagnostic.rb +0 -2
  11. data/lib/rigor/analysis/fact_store.rb +11 -3
  12. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  13. data/lib/rigor/analysis/runner.rb +114 -3
  14. data/lib/rigor/builtins/imported_refinements.rb +360 -55
  15. data/lib/rigor/cache/descriptor.rb +1 -1
  16. data/lib/rigor/cache/store.rb +1 -1
  17. data/lib/rigor/cli/diff_command.rb +1 -1
  18. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  19. data/lib/rigor/cli/type_of_command.rb +1 -1
  20. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  21. data/lib/rigor/cli/type_scan_report.rb +2 -2
  22. data/lib/rigor/cli.rb +9 -1
  23. data/lib/rigor/configuration/dependencies.rb +2 -2
  24. data/lib/rigor/configuration.rb +2 -2
  25. data/lib/rigor/environment.rb +35 -4
  26. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  27. data/lib/rigor/flow_contribution/element.rb +1 -1
  28. data/lib/rigor/flow_contribution/fact.rb +1 -1
  29. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  30. data/lib/rigor/flow_contribution/merger.rb +3 -3
  31. data/lib/rigor/flow_contribution.rb +2 -2
  32. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  33. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  34. data/lib/rigor/inference/expression_typer.rb +67 -11
  35. data/lib/rigor/inference/fallback.rb +1 -1
  36. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  37. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
  38. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  39. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  40. data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
  41. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
  42. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  43. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
  44. data/lib/rigor/inference/method_dispatcher.rb +146 -2
  45. data/lib/rigor/inference/method_parameter_binder.rb +1 -3
  46. data/lib/rigor/inference/narrowing.rb +2 -4
  47. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  48. data/lib/rigor/inference/scope_indexer.rb +14 -9
  49. data/lib/rigor/inference/statement_evaluator.rb +7 -7
  50. data/lib/rigor/plugin/io_boundary.rb +0 -2
  51. data/lib/rigor/plugin/loader.rb +2 -2
  52. data/lib/rigor/plugin/manifest.rb +30 -9
  53. data/lib/rigor/plugin/registry.rb +11 -0
  54. data/lib/rigor/plugin/services.rb +1 -1
  55. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  56. data/lib/rigor/plugin.rb +1 -0
  57. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  58. data/lib/rigor/rbs_extended.rb +131 -32
  59. data/lib/rigor/scope.rb +25 -8
  60. data/lib/rigor/sig_gen/classification.rb +36 -0
  61. data/lib/rigor/sig_gen/generator.rb +1048 -0
  62. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  63. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  64. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  65. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  66. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  67. data/lib/rigor/sig_gen/renderer.rb +157 -0
  68. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  69. data/lib/rigor/sig_gen/write_result.rb +48 -0
  70. data/lib/rigor/sig_gen/writer.rb +530 -0
  71. data/lib/rigor/sig_gen.rb +25 -0
  72. data/lib/rigor/type/bound_method.rb +79 -0
  73. data/lib/rigor/type/combinator.rb +195 -2
  74. data/lib/rigor/type/constant.rb +13 -0
  75. data/lib/rigor/type/hash_shape.rb +0 -2
  76. data/lib/rigor/type/union.rb +20 -1
  77. data/lib/rigor/type.rb +1 -0
  78. data/lib/rigor/type_node/generic.rb +62 -0
  79. data/lib/rigor/type_node/identifier.rb +30 -0
  80. data/lib/rigor/type_node/indexed_access.rb +41 -0
  81. data/lib/rigor/type_node/integer_literal.rb +29 -0
  82. data/lib/rigor/type_node/name_scope.rb +52 -0
  83. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  84. data/lib/rigor/type_node/string_literal.rb +29 -0
  85. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  86. data/lib/rigor/type_node/union.rb +42 -0
  87. data/lib/rigor/type_node.rb +29 -0
  88. data/lib/rigor/version.rb +1 -1
  89. data/lib/rigor.rb +2 -0
  90. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  91. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  92. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  93. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  94. data/sig/rigor/cli/diff_command.rbs +4 -0
  95. data/sig/rigor/cli/explain_command.rbs +4 -0
  96. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  97. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  98. data/sig/rigor/environment.rbs +5 -2
  99. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  100. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  101. data/sig/rigor/inference/builtins.rbs +2 -0
  102. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  103. data/sig/rigor/plugin/base.rbs +6 -0
  104. data/sig/rigor/plugin/fact_store.rbs +11 -0
  105. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  106. data/sig/rigor/plugin/load_error.rbs +6 -0
  107. data/sig/rigor/plugin/loader.rbs +20 -0
  108. data/sig/rigor/plugin/manifest.rbs +9 -0
  109. data/sig/rigor/plugin/registry.rbs +3 -0
  110. data/sig/rigor/plugin/services.rbs +3 -0
  111. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  112. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  113. data/sig/rigor/plugin.rbs +8 -0
  114. data/sig/rigor/scope.rbs +4 -2
  115. data/sig/rigor/type.rbs +28 -6
  116. metadata +52 -1
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optionparser"
4
+
5
+ require_relative "../configuration"
6
+ require_relative "../sig_gen"
7
+
8
+ module Rigor
9
+ class CLI
10
+ # Executes the `rigor sig-gen` command — ADR-14 slices 1–3.
11
+ #
12
+ # Walks the given paths (or `configuration.paths` when none
13
+ # are supplied), classifies every reachable instance method
14
+ # via {Rigor::SigGen::Generator}, and either prints the
15
+ # resulting RBS skeletons / unified-style diffs (`--print`,
16
+ # `--diff`; slice 1) or writes them to the project signature
17
+ # tree via {Rigor::SigGen::Writer} (`--write`; slice 2).
18
+ #
19
+ # `--write` follows the established Ruby community
20
+ # convention: `lib/foo/bar.rb` → `sig/foo/bar.rbs`. New
21
+ # methods are inserted into the matching class declaration
22
+ # just before its closing `end`; new classes are appended
23
+ # to the file; non-existent target files are created. User-
24
+ # authored declarations are NEVER replaced unless
25
+ # `--overwrite` is set AND the candidate is a
26
+ # `tighter-return`.
27
+ #
28
+ # Parameter policy defaults to `untyped`. `--params=observed`
29
+ # (slice 3) opts in to caller-side observation harvesting:
30
+ # the `ObservationCollector` walks `--observe=PATH...`
31
+ # (default `spec/` when no flag is given AND a `spec/`
32
+ # directory exists), unions per-position arg types, and the
33
+ # generator emits the union per ADR-5 clause 2.
34
+ # `--params=observed-strict` stays reserved-but-inert until
35
+ # the capability-role catalog ships (rejected with a usage
36
+ # error so the surface stays stable).
37
+ class SigGenCommand
38
+ USAGE = "Usage: rigor sig-gen [options] [paths]"
39
+
40
+ VALID_MODES = %w[print diff write].freeze
41
+ VALID_PARAM_POLICIES = %w[untyped observed observed-strict].freeze
42
+ VALID_FORMATS = %w[text json].freeze
43
+
44
+ def initialize(argv:, out:, err:)
45
+ @argv = argv
46
+ @out = out
47
+ @err = err
48
+ end
49
+
50
+ # @return [Integer] CLI exit status.
51
+ def run
52
+ options = parse_options
53
+ return CLI::EXIT_USAGE if options.nil?
54
+
55
+ configuration = Configuration.load(options.fetch(:config))
56
+ paths = @argv.empty? ? configuration.paths : @argv
57
+
58
+ observations = collect_observations(configuration, options)
59
+ candidates = SigGen::Generator.new(configuration: configuration, paths: paths,
60
+ observations: observations,
61
+ include_private: options.fetch(:include_private)).run
62
+ mode = options.fetch(:mode).to_sym
63
+
64
+ if mode == :write
65
+ dispatch_write(candidates, configuration, options)
66
+ else
67
+ dispatch_print_or_diff(candidates, mode, options)
68
+ end
69
+ 0
70
+ end
71
+
72
+ private
73
+
74
+ def dispatch_print_or_diff(candidates, mode, options)
75
+ SigGen::Renderer.new(out: @out).render(
76
+ candidates: candidates,
77
+ mode: mode,
78
+ format: options.fetch(:format),
79
+ selection: options.fetch(:selection)
80
+ )
81
+ end
82
+
83
+ def dispatch_write(candidates, configuration, options)
84
+ layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
85
+ path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
86
+ writer = SigGen::Writer.new(path_mapper: path_mapper, overwrite: options.fetch(:overwrite))
87
+
88
+ results = writer.write_all(candidates)
89
+
90
+ SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format))
91
+ end
92
+
93
+ # Slice 3 — collect call-site argument observations when
94
+ # `--params=observed` is set. When `--observe=PATH` is
95
+ # not specified, default to `spec/` (skipped silently
96
+ # when the directory is absent).
97
+ def collect_observations(configuration, options)
98
+ return {} if options.fetch(:params) != "observed"
99
+
100
+ observe_paths = options.fetch(:observe)
101
+ observe_paths = ["spec"] if observe_paths.empty? && File.directory?("spec")
102
+ SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths).collect
103
+ end
104
+
105
+ def parse_options
106
+ options = {
107
+ mode: "print",
108
+ format: "text",
109
+ params: "untyped",
110
+ selection: [],
111
+ overwrite: false,
112
+ observe: [],
113
+ include_private: false,
114
+ config: nil
115
+ }
116
+ build_option_parser(options).parse!(@argv)
117
+
118
+ message = validation_error(options)
119
+ return options if message.nil?
120
+
121
+ @err.puts("sig-gen: #{message}")
122
+ nil
123
+ end
124
+
125
+ def build_option_parser(options) # rubocop:disable Metrics/AbcSize
126
+ OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
127
+ opts.banner = USAGE
128
+ opts.on("--print", "Write RBS skeletons to stdout (default)") { options[:mode] = "print" }
129
+ opts.on("--diff", "Write a unified diff against existing RBS") { options[:mode] = "diff" }
130
+ opts.on("--write", "Write generated RBS to sig/<path>.rbs files") { options[:mode] = "write" }
131
+ opts.on("--overwrite", "Allow tighter-return updates to replace user-authored RBS") do
132
+ options[:overwrite] = true
133
+ end
134
+ opts.on("--include-private", "Emit private / protected instance methods (default: public only)") do
135
+ options[:include_private] = true
136
+ end
137
+ opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
138
+ opts.on("--params=POLICY", "Parameter policy: untyped (default), observed, observed-strict") do |value|
139
+ options[:params] = value
140
+ end
141
+ opts.on("--observe=PATH", "Directory / file to scan for call-site observations (repeatable)") do |value|
142
+ options[:observe] << value
143
+ end
144
+ opts.on("--new-files", "Emit only new-file classifications") do
145
+ options[:selection] << SigGen::Classification::NEW_FILE
146
+ end
147
+ opts.on("--new-methods", "Emit only new-method classifications") do
148
+ options[:selection] << SigGen::Classification::NEW_METHOD
149
+ end
150
+ opts.on("--tighter-returns", "Emit only tighter-return classifications") do
151
+ options[:selection] << SigGen::Classification::TIGHTER_RETURN
152
+ end
153
+ opts.on("--config=PATH", "Path to the Rigor configuration file") { |value| options[:config] = value }
154
+ end
155
+ end
156
+
157
+ def validation_error(options)
158
+ mode = options.fetch(:mode)
159
+ format = options.fetch(:format)
160
+ params = options.fetch(:params)
161
+
162
+ return "--print, --diff, and --write are mutually exclusive flags; pick one" unless VALID_MODES.include?(mode)
163
+ return "unsupported --format=#{format}" unless VALID_FORMATS.include?(format)
164
+ return "unsupported --params=#{params}" unless VALID_PARAM_POLICIES.include?(params)
165
+ if params == "observed-strict"
166
+ return "--params=observed-strict is reserved until the capability-role catalog ships"
167
+ end
168
+
169
+ nil
170
+ end
171
+ end
172
+ end
173
+ end
@@ -61,7 +61,7 @@ module Rigor
61
61
  options
62
62
  end
63
63
 
64
- def execute(target:, options:) # rubocop:disable Metrics/AbcSize
64
+ def execute(target:, options:)
65
65
  file, line, column = target
66
66
  return 1 unless file_exists?(file)
67
67
 
@@ -10,7 +10,7 @@ module Rigor
10
10
  # branches share a single source of truth (the `Report` value object) so
11
11
  # the two formats stay in lockstep; that pairing is why this class is a
12
12
  # bit longer than the default class-length budget.
13
- class TypeScanRenderer # rubocop:disable Metrics/ClassLength
13
+ class TypeScanRenderer
14
14
  def initialize(out:)
15
15
  @out = out
16
16
  end
@@ -5,14 +5,14 @@ module Rigor
5
5
  # Aggregated report assembled by `TypeScanCommand` and consumed by
6
6
  # `TypeScanRenderer`. The struct holds per-file paths, accumulated
7
7
  # per-class counts, located fallback events, and any parse errors.
8
- Report = Data.define(
8
+ class Report < Data.define(
9
9
  :files,
10
10
  :parse_errors,
11
11
  :visits,
12
12
  :unrecognized,
13
13
  :events,
14
14
  :options
15
- ) do
15
+ )
16
16
  def visited_count
17
17
  visits.values.sum
18
18
  end
data/lib/rigor/cli.rb CHANGED
@@ -24,7 +24,8 @@ module Rigor
24
24
  "type-of" => :run_type_of,
25
25
  "type-scan" => :run_type_scan,
26
26
  "explain" => :run_explain,
27
- "diff" => :run_diff
27
+ "diff" => :run_diff,
28
+ "sig-gen" => :run_sig_gen
28
29
  }.freeze
29
30
 
30
31
  def self.start(argv = ARGV, out: $stdout, err: $stderr)
@@ -279,6 +280,12 @@ module Rigor
279
280
  DiffCommand.new(argv: @argv, out: @out, err: @err).run
280
281
  end
281
282
 
283
+ def run_sig_gen
284
+ require_relative "cli/sig_gen_command"
285
+
286
+ SigGenCommand.new(argv: @argv, out: @out, err: @err).run
287
+ end
288
+
282
289
  def write_result(result, format)
283
290
  case format
284
291
  when "json"
@@ -318,6 +325,7 @@ module Rigor
318
325
  type-scan Report Scope#type_of coverage across PATHs
319
326
  explain Print the description of one or all CheckRules
320
327
  diff Compare current diagnostics to a saved baseline JSON
328
+ sig-gen Emit RBS skeletons inferred from .rb sources (ADR-14)
321
329
  version Print the Rigor version
322
330
  help Print this help
323
331
  HELP
@@ -13,7 +13,7 @@ module Rigor
13
13
  # is read, but no analyzer machinery consumes it yet. Slice 2
14
14
  # wires `Analysis::DependencySourceInference` against this
15
15
  # value object.
16
- class Dependencies # rubocop:disable Metrics/ClassLength
16
+ class Dependencies
17
17
  # Walking modes per
18
18
  # [ADR-10 § "Decision"](../../../docs/adr/10-dependency-source-inference.md#decision).
19
19
  VALID_MODES = %i[disabled when_missing full].freeze
@@ -61,7 +61,7 @@ module Rigor
61
61
  # walk time); `mode:` is one of {VALID_MODES}; `roots:` is
62
62
  # the list of subdirectories within the gem's installation
63
63
  # directory to walk (defaults to `["lib"]`).
64
- Entry = Data.define(:gem, :mode, :roots) do
64
+ class Entry < Data.define(:gem, :mode, :roots)
65
65
  def disabled? = mode == :disabled
66
66
  def when_missing? = mode == :when_missing
67
67
  def full? = mode == :full
@@ -214,7 +214,7 @@ module Rigor
214
214
  private_class_method :load_with_includes, :merge_includes, :resolve_paths_in, :deep_merge,
215
215
  :merge_value, :merge_dependencies_hash
216
216
 
217
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
217
+ # rubocop:disable Metrics/AbcSize
218
218
  def initialize(data = DEFAULTS)
219
219
  cache = DEFAULTS.fetch("cache").merge(data.fetch("cache", {}))
220
220
  plugins_io = DEFAULTS.fetch("plugins_io").merge(data.fetch("plugins_io", {}))
@@ -247,7 +247,7 @@ module Rigor
247
247
  data.fetch("dependencies", DEFAULTS.fetch("dependencies"))
248
248
  )
249
249
  end
250
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
250
+ # rubocop:enable Metrics/AbcSize
251
251
 
252
252
  def to_h
253
253
  {
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative "environment/class_registry"
4
4
  require_relative "environment/rbs_loader"
5
+ require_relative "type_node/name_scope"
6
+ require_relative "type_node/resolver_chain"
5
7
 
6
8
  module Rigor
7
9
  # The engine's view of the type universe outside the current scope.
@@ -41,7 +43,8 @@ module Rigor
41
43
  prism rbs
42
44
  ].freeze
43
45
 
44
- attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index
46
+ attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
47
+ :rbs_extended_reporter, :boundary_cross_reporter, :name_scope
45
48
 
46
49
  # @param class_registry [Rigor::Environment::ClassRegistry]
47
50
  # @param rbs_loader [Rigor::Environment::RbsLoader, nil] when nil the
@@ -63,11 +66,15 @@ module Rigor
63
66
  # When nil (the default), no dep-source contribution
64
67
  # participates and the dispatcher tier is a no-op.
65
68
  def initialize(class_registry: ClassRegistry.default, rbs_loader: nil,
66
- plugin_registry: nil, dependency_source_index: nil)
69
+ plugin_registry: nil, dependency_source_index: nil,
70
+ rbs_extended_reporter: nil, boundary_cross_reporter: nil)
67
71
  @class_registry = class_registry
68
72
  @rbs_loader = rbs_loader
69
73
  @plugin_registry = plugin_registry
70
74
  @dependency_source_index = dependency_source_index
75
+ @rbs_extended_reporter = rbs_extended_reporter
76
+ @boundary_cross_reporter = boundary_cross_reporter
77
+ @name_scope = build_name_scope
71
78
  freeze
72
79
  end
73
80
 
@@ -98,7 +105,8 @@ module Rigor
98
105
  # default) to skip caching for this environment.
99
106
  # @return [Rigor::Environment]
100
107
  def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil, # rubocop:disable Metrics/ParameterLists
101
- plugin_registry: nil, dependency_source_index: nil)
108
+ plugin_registry: nil, dependency_source_index: nil,
109
+ rbs_extended_reporter: nil, boundary_cross_reporter: nil)
102
110
  resolved_paths = signature_paths || default_signature_paths(root)
103
111
  merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
104
112
  loader = RbsLoader.new(
@@ -109,7 +117,9 @@ module Rigor
109
117
  new(
110
118
  rbs_loader: loader,
111
119
  plugin_registry: plugin_registry,
112
- dependency_source_index: dependency_source_index
120
+ dependency_source_index: dependency_source_index,
121
+ rbs_extended_reporter: rbs_extended_reporter,
122
+ boundary_cross_reporter: boundary_cross_reporter
113
123
  )
114
124
  end
115
125
 
@@ -204,5 +214,26 @@ module Rigor
204
214
  def normalize_class_name(name)
205
215
  name.to_s.delete_prefix("::")
206
216
  end
217
+
218
+ # ADR-13 slice 3b — composes the per-run plugin-supplied
219
+ # {Rigor::TypeNode::ResolverChain} into a single
220
+ # {Rigor::TypeNode::NameScope} that the RBS::Extended
221
+ # directive parser threads down to the
222
+ # {Rigor::Builtins::ImportedRefinements::Resolver}. Returns
223
+ # `nil` when no plugin contributes a type-node resolver so
224
+ # the parser short-circuits the chain consultation and
225
+ # behaves bit-for-bit like the v0.1.0 → v0.1.3 default.
226
+ def build_name_scope
227
+ return nil if @plugin_registry.nil? || @plugin_registry.empty?
228
+
229
+ resolvers = @plugin_registry.type_node_resolvers
230
+ return nil if resolvers.empty?
231
+
232
+ TypeNode::NameScope.new(
233
+ resolver: TypeNode::ResolverChain.new(resolvers),
234
+ class_context: nil,
235
+ type_alias_table: {}
236
+ )
237
+ end
207
238
  end
208
239
  end
@@ -28,8 +28,8 @@ module Rigor
28
28
  lower_tier_contradiction
29
29
  ].freeze
30
30
 
31
- Conflict = Data.define(:target, :edge, :kind, :reason, :provenances, :message) do
32
- def initialize(target:, edge:, kind:, reason:, provenances:, message:) # rubocop:disable Metrics/ParameterLists
31
+ class Conflict < Data.define(:target, :edge, :kind, :reason, :provenances, :message)
32
+ def initialize(target:, edge:, kind:, reason:, provenances:, message:)
33
33
  unless CONFLICT_VALID_REASONS.include?(reason)
34
34
  raise ArgumentError,
35
35
  "FlowContribution::Conflict reason must be one of " \
@@ -28,7 +28,7 @@ module Rigor
28
28
  role
29
29
  ].freeze
30
30
 
31
- Element = Data.define(:target, :edge, :kind, :payload, :provenance) do
31
+ class Element < Data.define(:target, :edge, :kind, :payload, :provenance)
32
32
  def initialize(target:, edge:, kind:, payload:, provenance:)
33
33
  unless ELEMENT_VALID_EDGES.include?(edge)
34
34
  raise ArgumentError,
@@ -55,7 +55,7 @@ module Rigor
55
55
  # land in the same merge bucket.
56
56
  FACT_VALID_TARGET_KINDS = %i[parameter self].freeze
57
57
 
58
- Fact = Data.define(:target_kind, :target_name, :type, :negative) do
58
+ class Fact < Data.define(:target_kind, :target_name, :type, :negative)
59
59
  def initialize(target_kind:, target_name:, type:, negative: false)
60
60
  unless FACT_VALID_TARGET_KINDS.include?(target_kind)
61
61
  raise ArgumentError,
@@ -42,7 +42,7 @@ module Rigor
42
42
  !@conflicts.empty?
43
43
  end
44
44
 
45
- def empty? # rubocop:disable Metrics/CyclomaticComplexity
45
+ def empty?
46
46
  @return_type.nil? && @truthy_facts.empty? && @falsey_facts.empty? &&
47
47
  @post_return_facts.empty? && @mutations.empty? && @invalidations.empty? &&
48
48
  @exceptional.nil? && @role_conformance.empty?
@@ -55,7 +55,7 @@ module Rigor
55
55
  # In every conflict case the result keeps the higher-tier value
56
56
  # for that slot, records a {Conflict} with both provenances, and
57
57
  # continues processing the remaining slots / contributions.
58
- module Merger # rubocop:disable Metrics/ModuleLength
58
+ module Merger
59
59
  AUTHORITY_TIERS = {
60
60
  builtin: 0,
61
61
  rbs_extended: 1,
@@ -112,7 +112,7 @@ module Rigor
112
112
  fold_role_conformance(state, contribution)
113
113
  end
114
114
 
115
- def fold_return_type(state, contribution, tier) # rubocop:disable Metrics/AbcSize
115
+ def fold_return_type(state, contribution, tier)
116
116
  incoming = contribution.return_type
117
117
  return if incoming.nil?
118
118
 
@@ -202,7 +202,7 @@ module Rigor
202
202
  end
203
203
  end
204
204
 
205
- def build_conflict(target:, edge:, kind:, reason:, provenances:, message:) # rubocop:disable Metrics/ParameterLists
205
+ def build_conflict(target:, edge:, kind:, reason:, provenances:, message:)
206
206
  Conflict.new(target: target, edge: edge, kind: kind, reason: reason,
207
207
  provenances: provenances, message: message)
208
208
  end
@@ -32,7 +32,7 @@ module Rigor
32
32
  # `descriptor` is the {Rigor::Cache::Descriptor} this
33
33
  # contribution attaches to (or `nil` when the contribution does
34
34
  # not need its own cache slice).
35
- Provenance = Data.define(:source_family, :plugin_id, :node, :descriptor) do
35
+ class Provenance < Data.define(:source_family, :plugin_id, :node, :descriptor)
36
36
  def self.builtin
37
37
  new(source_family: :builtin, plugin_id: nil, node: nil, descriptor: nil)
38
38
  end
@@ -122,7 +122,7 @@ module Rigor
122
122
  # | role_conformance | normal | role | (per-role target) |
123
123
  #
124
124
  # @return [Array<Element>]
125
- def to_element_list # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
125
+ def to_element_list # rubocop:disable Metrics/AbcSize
126
126
  elements = []
127
127
  elements << element_for(:return, :normal, :return_type, return_type) unless return_type.nil?
128
128
  Array(truthy_facts).each { |fact| elements << element_for(fact_target(fact), :truthy, :truthy_fact, fact) }
@@ -45,7 +45,6 @@ module Rigor
45
45
  # scope MUST NOT observe them and the binder leaves them unbound.
46
46
  #
47
47
  # See docs/internal-spec/inference-engine.md for the binding contract.
48
- # rubocop:disable Metrics/ClassLength
49
48
  class BlockParameterBinder
50
49
  # @param expected_param_types [Array<Rigor::Type>] positional block
51
50
  # parameter types in order. Indices the binder cannot fill from
@@ -208,6 +207,5 @@ module Rigor
208
207
  @expected_param_types[index] || Type::Combinator.untyped
209
208
  end
210
209
  end
211
- # rubocop:enable Metrics/ClassLength
212
210
  end
213
211
  end
@@ -24,7 +24,7 @@ module Rigor
24
24
  # hot inference path: it allocates a tracer per visited node and discards
25
25
  # the inferred type values.
26
26
  class CoverageScanner
27
- Result = Data.define(:visits, :unrecognized, :events) do
27
+ class Result < Data.define(:visits, :unrecognized, :events)
28
28
  # @return [Integer] sum of all visits across node classes.
29
29
  def visited_count
30
30
  visits.values.sum
@@ -111,7 +111,21 @@ module Rigor
111
111
  Prism::IndexOrWriteNode => :type_of_assignment_write,
112
112
  Prism::IndexAndWriteNode => :type_of_assignment_write,
113
113
  Prism::MultiWriteNode => :type_of_assignment_write,
114
+ # LHS-only target nodes (destructuring assignment, pattern matching,
115
+ # `for x in xs`, block parameter `|a, (b, c)|`). They have no value
116
+ # to extract — the type-of pass acknowledges the node class so the
117
+ # coverage scanner stops flagging it; binding the inner names back
118
+ # into the scope is the StatementEvaluator / MultiTargetBinder /
119
+ # BlockParameterBinder side's concern.
114
120
  Prism::LocalVariableTargetNode => :type_of_non_value,
121
+ Prism::MultiTargetNode => :type_of_non_value,
122
+ Prism::InstanceVariableTargetNode => :type_of_non_value,
123
+ Prism::ClassVariableTargetNode => :type_of_non_value,
124
+ Prism::GlobalVariableTargetNode => :type_of_non_value,
125
+ Prism::ConstantTargetNode => :type_of_non_value,
126
+ Prism::ConstantPathTargetNode => :type_of_non_value,
127
+ Prism::CallTargetNode => :type_of_non_value,
128
+ Prism::IndexTargetNode => :type_of_non_value,
115
129
  # Hashes and interpolation
116
130
  Prism::HashNode => :type_of_hash,
117
131
  Prism::KeywordHashNode => :type_of_hash,
@@ -931,7 +945,6 @@ module Rigor
931
945
  # for the CallNode itself (the inner type_of calls already record
932
946
  # their own fallbacks for unrecognised receivers/args, so the tracer
933
947
  # captures both the immediate dispatch miss and the deeper cause).
934
- # rubocop:disable Metrics/CyclomaticComplexity
935
948
  def call_type_for(node)
936
949
  receiver = call_receiver_type_for(node)
937
950
  arg_types = call_arg_types(node)
@@ -1004,7 +1017,6 @@ module Rigor
1004
1017
 
1005
1018
  fallback_for(node, family: :prism)
1006
1019
  end
1007
- # rubocop:enable Metrics/CyclomaticComplexity
1008
1020
 
1009
1021
  # v0.0.2 #5 — re-types the body of a user-defined
1010
1022
  # instance method with the call site's argument types
@@ -1160,9 +1172,20 @@ module Rigor
1160
1172
  # when typing the body raises (defensive against malformed
1161
1173
  # subtrees); the dispatcher then runs in its no-block-aware
1162
1174
  # path.
1175
+ #
1176
+ # ADR-14 gap-#3 (d): a `Prism::BlockArgumentNode` carrying
1177
+ # `&:symbol` (the Symbol#to_proc shorthand) is treated as
1178
+ # a block. The block's return type is computed by
1179
+ # dispatching `:symbol` on the expected block param type
1180
+ # (per `Symbol#to_proc`'s `{ |x| x.symbol }` semantics).
1181
+ # A precise inner dispatch produces the right return; any
1182
+ # failure step falls back to `Dynamic[Top]` so the
1183
+ # dispatcher still SEES a block — selecting the block-
1184
+ # bearing overload of e.g. `Hash#transform_values` over
1185
+ # the no-block overload that returns `Enumerator`.
1163
1186
  def block_return_type_for(call_node, receiver_type, arg_types)
1164
- block_node = call_node.block
1165
- return nil unless block_node.is_a?(Prism::BlockNode)
1187
+ block_arg = call_node.block
1188
+ return nil if block_arg.nil?
1166
1189
  return nil if receiver_type.nil?
1167
1190
 
1168
1191
  expected = MethodDispatcher.expected_block_param_types(
@@ -1171,13 +1194,50 @@ module Rigor
1171
1194
  arg_types: arg_types,
1172
1195
  environment: scope.environment
1173
1196
  )
1174
- bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
1175
- block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
1176
- type_block_body(block_node, block_scope)
1197
+ block_return_for(block_arg, expected)
1177
1198
  rescue StandardError
1178
1199
  nil
1179
1200
  end
1180
1201
 
1202
+ def block_return_for(block_arg, expected)
1203
+ case block_arg
1204
+ when Prism::BlockNode
1205
+ bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_arg)
1206
+ block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
1207
+ type_block_body(block_arg, block_scope)
1208
+ when Prism::BlockArgumentNode
1209
+ symbol_block_return_type(block_arg, expected)
1210
+ end
1211
+ end
1212
+
1213
+ # `&:symbol` desugars to a one-arg Proc that dispatches
1214
+ # `symbol` against its argument. When the param type is
1215
+ # known and the resulting inner dispatch is precise,
1216
+ # this returns the precise carrier; otherwise it
1217
+ # returns `Dynamic[Top]` (still non-nil) so the outer
1218
+ # dispatcher selects the block-bearing overload.
1219
+ # `&proc_local` / `&method(:foo)` and friends — anything
1220
+ # not a bare SymbolNode — still resolve to
1221
+ # `Dynamic[Top]` for the same block-presence signal.
1222
+ def symbol_block_return_type(block_arg, expected_param_types)
1223
+ expression = block_arg.expression
1224
+ return dynamic_top unless expression.is_a?(Prism::SymbolNode)
1225
+
1226
+ param_type = expected_param_types&.first
1227
+ return dynamic_top if param_type.nil?
1228
+
1229
+ result = MethodDispatcher.dispatch(
1230
+ receiver_type: param_type,
1231
+ method_name: expression.unescaped.to_sym,
1232
+ arg_types: [],
1233
+ block_type: nil,
1234
+ environment: scope.environment,
1235
+ call_node: block_arg,
1236
+ scope: scope
1237
+ )
1238
+ result || dynamic_top
1239
+ end
1240
+
1181
1241
  def type_block_body(block_node, block_scope)
1182
1242
  body = block_node.body
1183
1243
  return Type::Combinator.constant_of(nil) if body.nil?
@@ -1213,7 +1273,6 @@ module Rigor
1213
1273
  PER_ELEMENT_RANGE_LIMIT = 8
1214
1274
  private_constant :PER_ELEMENT_RANGE_LIMIT
1215
1275
 
1216
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1217
1276
  def try_per_element_block_fold(call_node, receiver_type)
1218
1277
  return nil unless PER_ELEMENT_TUPLE_METHODS.include?(call_node.name)
1219
1278
  return nil if find_family_with_args?(call_node)
@@ -1231,7 +1290,6 @@ module Rigor
1231
1290
 
1232
1291
  assemble_per_element_result(call_node.name, per_position, element_types)
1233
1292
  end
1234
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1235
1293
 
1236
1294
  # Returns the per-position element types for a finite,
1237
1295
  # statically-known receiver shape — or nil when the
@@ -1254,7 +1312,6 @@ module Rigor
1254
1312
  end
1255
1313
  end
1256
1314
 
1257
- # rubocop:disable Metrics/CyclomaticComplexity
1258
1315
  def constant_range_elements(value)
1259
1316
  return nil unless value.is_a?(Range)
1260
1317
  return nil unless value.begin.is_a?(Integer) && value.end.is_a?(Integer)
@@ -1264,7 +1321,6 @@ module Rigor
1264
1321
 
1265
1322
  value.to_a.map { |v| Type::Combinator.constant_of(v) }
1266
1323
  end
1267
- # rubocop:enable Metrics/CyclomaticComplexity
1268
1324
 
1269
1325
  # `index(value)` and `find_index(value)` carry a positional
1270
1326
  # argument and search by `==` rather than running the block.
@@ -20,7 +20,7 @@ module Rigor
20
20
  # - inner_type: the Rigor::Type returned to the caller (currently
21
21
  # always Dynamic[Top]; later slices may carry richer fallback
22
22
  # types).
23
- Fallback = Data.define(:node_class, :location, :family, :inner_type) do
23
+ class Fallback < Data.define(:node_class, :location, :family, :inner_type)
24
24
  def initialize(node_class:, location:, family:, inner_type:)
25
25
  raise ArgumentError, "node_class must be a Class, got #{node_class.class}" unless node_class.is_a?(Class)
26
26