rigortype 0.4.5 → 0.4.6

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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docs/handbook/11-sig-gen.md +11 -2
  4. data/docs/manual/02-cli-reference.md +46 -7
  5. data/docs/manual/03-configuration.md +2 -1
  6. data/docs/manual/04-diagnostics.md +1 -1
  7. data/docs/manual/10-mcp-server.md +9 -1
  8. data/docs/manual/12-caching.md +8 -1
  9. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  10. data/docs/manual/plugins/rigor-actionpack.md +30 -0
  11. data/docs/manual/plugins/rigor-devise.md +15 -0
  12. data/docs/manual/plugins/rigor-rspec.md +2 -1
  13. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  14. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
  15. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
  16. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -0
  17. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  18. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  19. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  20. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  21. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +51 -0
  22. data/lib/rigor/analysis/check_rules.rb +93 -8
  23. data/lib/rigor/analysis/dependency_recorder.rb +11 -0
  24. data/lib/rigor/analysis/edit_replay.rb +67 -0
  25. data/lib/rigor/analysis/incremental.rb +37 -0
  26. data/lib/rigor/analysis/incremental_session.rb +175 -27
  27. data/lib/rigor/analysis/path_expansion.rb +41 -0
  28. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  29. data/lib/rigor/analysis/rule_catalog.rb +5 -1
  30. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  31. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  32. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  33. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
  34. data/lib/rigor/analysis/runner/pool_coordinator.rb +17 -4
  35. data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
  36. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  37. data/lib/rigor/analysis/runner.rb +156 -49
  38. data/lib/rigor/analysis/worker_session.rb +14 -2
  39. data/lib/rigor/cache/incremental_snapshot.rb +30 -7
  40. data/lib/rigor/cli/check_command.rb +93 -4
  41. data/lib/rigor/cli/diff_command.rb +2 -1
  42. data/lib/rigor/cli/lens_command.rb +5 -5
  43. data/lib/rigor/cli/mcp_command.rb +2 -1
  44. data/lib/rigor/cli/options.rb +20 -3
  45. data/lib/rigor/cli/plugins_command.rb +5 -0
  46. data/lib/rigor/cli/plugins_renderer.rb +24 -2
  47. data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
  48. data/lib/rigor/cli/sig_gen_command.rb +46 -7
  49. data/lib/rigor/cli/type_of_command.rb +2 -1
  50. data/lib/rigor/cli/type_of_project_scope.rb +27 -0
  51. data/lib/rigor/config_audit.rb +16 -5
  52. data/lib/rigor/configuration.rb +14 -4
  53. data/lib/rigor/environment/rbs_loader.rb +115 -6
  54. data/lib/rigor/environment/required_features.rb +136 -29
  55. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  56. data/lib/rigor/environment.rb +4 -2
  57. data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
  58. data/lib/rigor/inference/deep_erasure.rb +138 -0
  59. data/lib/rigor/inference/def_return_typer.rb +159 -9
  60. data/lib/rigor/inference/definer_resolution.rb +17 -3
  61. data/lib/rigor/inference/error_info.rb +17 -3
  62. data/lib/rigor/inference/expression_typer.rb +62 -13
  63. data/lib/rigor/inference/global_write_census.rb +36 -1
  64. data/lib/rigor/inference/ivar_writer_census.rb +195 -0
  65. data/lib/rigor/inference/loop_value_record.rb +53 -0
  66. data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
  67. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
  68. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
  69. data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
  70. data/lib/rigor/inference/method_dispatcher.rb +46 -16
  71. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  72. data/lib/rigor/inference/module_function_state.rb +12 -0
  73. data/lib/rigor/inference/mutation_widening.rb +5 -2
  74. data/lib/rigor/inference/narrowing.rb +66 -5
  75. data/lib/rigor/inference/object_mixins.rb +13 -1
  76. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  77. data/lib/rigor/inference/project_yielder.rb +128 -0
  78. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
  79. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  80. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
  81. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  82. data/lib/rigor/inference/scope_indexer.rb +480 -103
  83. data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
  84. data/lib/rigor/inference/statement_evaluator.rb +658 -90
  85. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  86. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  87. data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
  88. data/lib/rigor/language_server/buffer_table.rb +6 -0
  89. data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
  90. data/lib/rigor/language_server/project_context.rb +59 -3
  91. data/lib/rigor/language_server/server.rb +5 -3
  92. data/lib/rigor/mcp/server.rb +9 -1
  93. data/lib/rigor/plugin/base.rb +26 -11
  94. data/lib/rigor/plugin/manifest.rb +29 -3
  95. data/lib/rigor/plugin/registry.rb +38 -0
  96. data/lib/rigor/protection/discovery_seed.rb +57 -2
  97. data/lib/rigor/rooted_glob.rb +10 -1
  98. data/lib/rigor/scope.rb +14 -5
  99. data/lib/rigor/sig_gen/generator.rb +308 -37
  100. data/lib/rigor/sig_gen/observation_collector.rb +4 -1
  101. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  102. data/lib/rigor/sig_gen/renderer.rb +6 -3
  103. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  104. data/lib/rigor/type/combinator.rb +4 -3
  105. data/lib/rigor/type/constant.rb +45 -3
  106. data/lib/rigor/type/hash_shape.rb +14 -9
  107. data/lib/rigor/type/struct_instance.rb +3 -2
  108. data/lib/rigor/version.rb +1 -1
  109. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
  110. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
  111. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
  112. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
  113. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  114. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
  115. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  116. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
  117. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  118. data/sig/rigor/analysis/baseline.rbs +3 -2
  119. data/sig/rigor/effects/file_collection.rbs +5 -6
  120. data/sig/rigor/effects/registry.rbs +8 -6
  121. data/sig/rigor/effects/summary.rbs +6 -5
  122. data/sig/rigor/environment.rbs +5 -3
  123. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  124. data/sig/rigor/inference.rbs +6 -0
  125. data/sig/rigor/plugin/additional_initializer.rbs +5 -5
  126. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  127. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  128. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  129. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  130. data/sig/rigor/plugin/io_boundary.rbs +4 -4
  131. data/sig/rigor/plugin/manifest.rbs +16 -18
  132. data/sig/rigor/plugin/protocol_contract.rbs +10 -9
  133. data/sig/rigor/plugin/registry.rbs +1 -1
  134. data/sig/rigor/scope.rbs +31 -50
  135. data/sig/rigor/type.rbs +0 -6
  136. metadata +20 -1
@@ -14,6 +14,7 @@ require_relative "../source/literals"
14
14
  require_relative "../source/node_children"
15
15
  require_relative "../inference/def_return_typer"
16
16
  require_relative "../inference/scope_indexer"
17
+ require_relative "../inference/project_seed_tables"
17
18
  require_relative "../inference/module_function_state"
18
19
  require_relative "../inference/rbs_type_translator"
19
20
  require_relative "alias_index"
@@ -99,7 +100,7 @@ module Rigor
99
100
  # candidates from one file don't leak into another; initialising empty here gives downstream consumers
100
101
  # (`build_candidate`, `method_def_prefix`) a never-nil invariant without per-call-site defensive guards.
101
102
  @namespace_kinds = {}
102
- @module_function_methods = Set.new
103
+ @module_function_methods = Set.new.compare_by_identity
103
104
  @uncertain_module_function_defs = Set.new.compare_by_identity
104
105
  @class_shells = Set.new
105
106
  @class_superclasses = {}
@@ -126,6 +127,7 @@ module Rigor
126
127
 
127
128
  def run
128
129
  resolved = resolve_paths(@paths)
130
+ @resolved_paths = resolved
129
131
  @environment = build_environment(resolved)
130
132
  candidates = resolved.flat_map { |path| analyse_file(path, @environment) }
131
133
  annotate_effects(
@@ -458,22 +460,41 @@ module Rigor
458
460
  end.uniq
459
461
  end
460
462
 
463
+ # #1154 — the project's discovery tables, read only by the declared-parameter ivar credit: it must see the ivar
464
+ # writers in other files (an ancestor, another object's `instance_variable_set`, a reopening) as `rigor check`
465
+ # does. Built once per run over the configured project paths as well as the files sig-gen reads, so a run over
466
+ # one file declines what a `check` of it declines; every other part of the generator keeps its per-file scope.
467
+ def ivar_writer_scope(base_scope)
468
+ @ivar_writer_tables ||= Inference::ProjectSeedTables.for_paths(
469
+ ((@resolved_paths || []) + resolve_paths(Array(@configuration.paths))).uniq { |path| File.expand_path(path) }
470
+ )
471
+ base_scope.with_discovery(base_scope.discovery.with(**@ivar_writer_tables))
472
+ end
473
+
474
+ # #1406 — sig-gen keeps the census's own answer: a declared ivar type read back would echo the declaration.
475
+ def index_file(root, base_scope)
476
+ Inference::ScopeIndexer.index(
477
+ root, default_scope: base_scope, ivar_writer_scope: ivar_writer_scope(base_scope), declared_ivar_types: false
478
+ )
479
+ end
480
+
461
481
  def analyse_file(path, environment)
462
482
  source = File.read(path)
463
483
  parse_result = Prism.parse(source, filepath: path, version: @configuration.target_ruby)
464
484
  return [] if parse_result.errors.any?
465
485
 
466
- base_scope = Scope.empty(environment: environment)
467
- scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope)
486
+ scope_index = index_file(parse_result.value, Scope.empty(environment: environment))
468
487
 
469
488
  reset_file_maps
470
489
  @meta_layouts = collect_meta_layouts(scope_index)
471
490
  register_meta_classes
472
491
  defs = collect_method_definitions(parse_result.value, scope_index)
492
+ definers = definer_table(parse_result.value, defs)
493
+ defs = live_definitions(defs, definers)
473
494
  # Candidate construction freezes the per-file maps above (see {#build_candidate}), so every registration
474
495
  # pass has to be finished before the first `build_candidate` call. Meta members lead the RETURNED order —
475
496
  # a value class's members and constructors read first, ahead of the methods its block body defines.
476
- meta_candidates = collect_meta_member_candidates(path, scope_index)
497
+ meta_candidates = live_meta_candidates(path, scope_index, definers)
477
498
  candidates_from_defs = defs.filter_map do |def_node, class_name, kind|
478
499
  # An analyzer bug typing one def's body must cost only that def's candidate, never the whole
479
500
  # `rigor sig-gen` run. The `check` path recovers each *file* this way (worker_session.rb); sig-gen
@@ -485,13 +506,108 @@ module Rigor
485
506
  end
486
507
  obs_ivar_map = build_observed_ivar_map(parse_result.value)
487
508
  meta_candidates + candidates_from_defs +
488
- collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map)
509
+ collect_attr_candidates(parse_result.value, path, scope_index, obs_ivar_map, definers)
510
+ end
511
+
512
+ # Ruby keeps the LAST definition of a name: a `def x` followed by `attr_reader :x` (or the reverse) defines
513
+ # `x` once, and a second `def x` after a `module_function` reset replaces only the instance method. Writing
514
+ # one entry per definition gave RBS a duplicate method definition (#2008, #2023), so every definer of a
515
+ # `(class, kind, name)` slot is recorded with its source offset, and only the last one in a file is written.
516
+ # A module function defines two slots, its singleton copy and its instance method. A `Data.define` or
517
+ # `Struct.new` member is defined before any body that could redefine it, so it ranks first.
518
+ #
519
+ # Only a definer whose statement certainly runs can be last: a direct statement of a `class`, `module` or
520
+ # `class << self` body, of a `Data.define` / `Struct.new` block or of the file ({#walk_statements} records
521
+ # them). One under a condition, in a block or a lambda, passed to a call (`private def x`), or after an
522
+ # uncertain `module_function` reset may not run, as far as this reading tells. A slot such a definer could
523
+ # still take over from the last certain one is contested, and nothing is written for it.
524
+ #
525
+ # @return `{ [class_name, kind, method_name] => [[offset, certain], ...] }`.
526
+ MEMBER_OFFSET = -1
527
+ private_constant :MEMBER_OFFSET
528
+
529
+ def definer_table(root, defs)
530
+ certain = @certain_definers
531
+ table = Hash.new { |h, k| h[k] = [] }
532
+ record_def_definers(defs, certain, table)
533
+ meta_member_slots.each { |slot| table[slot] << [MEMBER_OFFSET, true] }
534
+ ctx = AttrWalkContext.new(definers: [])
535
+ walk_attr_calls(root, [], false, ctx)
536
+ ctx.definers.each do |slot, call_node|
537
+ table[slot] << [call_node.location.start_offset, certain.include?(call_node)]
538
+ end
539
+ table
540
+ end
541
+
542
+ def record_def_definers(defs, certain, table)
543
+ defs.each do |def_node, class_name, kind|
544
+ next if class_name.nil?
545
+
546
+ definer_slots(def_node, class_name, kind).each do |slot, possible_only|
547
+ table[slot] << [def_node.location.start_offset, !possible_only && certain.include?(def_node)]
548
+ end
549
+ end
550
+ end
551
+
552
+ def live_meta_candidates(path, scope_index, table)
553
+ collect_meta_member_candidates(path, scope_index).select do |c|
554
+ live_definer?(table, [c.class_name, c.kind, c.method_name], MEMBER_OFFSET)
555
+ end
556
+ end
557
+
558
+ # `[slot, possible_only]` pairs. A `def` after an uncertain `module_function` reset may or may not install a
559
+ # singleton copy, so it is a possible definer of that slot only.
560
+ def definer_slots(def_node, class_name, kind)
561
+ slots = [[[class_name, kind, def_node.name], false]]
562
+ if @module_function_methods.include?(def_node)
563
+ slots << [[class_name, :singleton, def_node.name], false]
564
+ elsif @uncertain_module_function_defs.include?(def_node)
565
+ slots << [[class_name, :singleton, def_node.name], true]
566
+ end
567
+ slots
568
+ end
569
+
570
+ def meta_member_slots
571
+ @meta_layouts.flat_map do |class_name, layout|
572
+ MetaClassShape.of(kind: layout.kind, members: layout.member_names, keyword_init: layout.keyword_init)
573
+ .member_decls.map { |member| [class_name, member.kind, member.method_name] }
574
+ end
575
+ end
576
+
577
+ # Whether the definer at `offset` is the one Ruby keeps for `slot`. A slot with a single definer keeps it,
578
+ # certain or not, as before.
579
+ def live_definer?(table, slot, offset)
580
+ entries = table.fetch(slot, [])
581
+ return true if entries.size <= 1
582
+
583
+ last_certain = entries.select { |_, certain| certain }.map(&:first).max
584
+ return false if last_certain.nil?
585
+ return false if entries.any? { |entry_offset, certain| !certain && entry_offset > last_certain }
586
+
587
+ offset == last_certain
588
+ end
589
+
590
+ # The `defs` whose definition Ruby keeps. A module function whose instance method a later definition
591
+ # replaced keeps only its singleton copy (`M.label` still returns the first body), so it is written as
592
+ # `def self.label`. One whose singleton copy was replaced keeps only a private instance method, which
593
+ # sig-gen does not write, so it is dropped.
594
+ def live_definitions(defs, table)
595
+ defs.filter_map do |def_node, class_name, kind|
596
+ next [def_node, class_name, kind] if class_name.nil?
597
+
598
+ slots = definer_slots(def_node, class_name, kind).reject(&:last).map(&:first)
599
+ live = slots.select { |slot| live_definer?(table, slot, def_node.location.start_offset) }
600
+ next [def_node, class_name, kind] if live.size == slots.size
601
+ next nil unless live == [[class_name, :singleton, def_node.name]]
602
+
603
+ [def_node, class_name, :singleton]
604
+ end
489
605
  end
490
606
 
491
607
  # The per-file scratch state {#analyse_file} starts each file with.
492
608
  def reset_file_maps
493
609
  @namespace_kinds = {}
494
- @module_function_methods = Set.new
610
+ @module_function_methods = Set.new.compare_by_identity
495
611
  @uncertain_module_function_defs = Set.new.compare_by_identity
496
612
  @class_shells = Set.new
497
613
  @class_superclasses = {}
@@ -509,15 +625,19 @@ module Rigor
509
625
  # - `@namespace_kinds[qualified_name]` records whether each segment came from `class Foo` (`:class`) or
510
626
  # `module Foo` (`:module`). Used by the writer's `wrap_in_modules` step to emit the right keyword for
511
627
  # each intermediate segment AND the leaf.
512
- # - `@module_function_methods` records `(class_name, method_name)` pairs where a `module_function` (no
513
- # args) call preceded the `def` inside a module body. The renderer emits `def self?.name` for these, the
514
- # RBS spelling that matches the dual instance + singleton dispatch the runtime produces.
628
+ # - `@module_function_methods` records the `def` nodes a `module_function` (no args) call preceded inside
629
+ # a module body. The renderer emits `def self?.name` for these, the RBS spelling that matches the dual
630
+ # instance + singleton dispatch the runtime produces. It is keyed by node, not by name: a later `def` of
631
+ # the same name after a reset is an ordinary method (#2023).
515
632
  def collect_method_definitions(root, scope_index)
516
633
  @declared_types = scope_index[root].declared_types
517
634
  @walk_root = root
518
635
  @in_effect_refinements = nil
519
636
  out = []
520
637
  @block_depth = 0
638
+ @certain_bodies = Set.new.compare_by_identity
639
+ @certain_definers = Set.new.compare_by_identity
640
+ mark_certain_body(root.statements)
521
641
  walk_defs(root, [], false, false, out)
522
642
  out
523
643
  end
@@ -530,6 +650,7 @@ module Rigor
530
650
  return if descend_into_namespace?(node, prefix, out)
531
651
  when Prism::SingletonClassNode
532
652
  if node.expression.is_a?(Prism::SelfNode) && node.body
653
+ mark_certain_body(node.body)
533
654
  walk_defs(node.body, prefix, true, false, out)
534
655
  return
535
656
  end
@@ -539,6 +660,7 @@ module Rigor
539
660
  when Prism::ConstantWriteNode
540
661
  body = meta_block_body(node, prefix)
541
662
  if body
663
+ mark_certain_body(body)
542
664
  walk_defs(body, prefix + [node.name.to_s], false, false, out)
543
665
  return
544
666
  end
@@ -717,13 +839,30 @@ module Rigor
717
839
  def walk_namespace_body(namespace_node, prefix, out)
718
840
  return if namespace_node.body.nil?
719
841
 
842
+ mark_certain_body(namespace_node.body)
720
843
  walk_defs(namespace_node.body, prefix, false, false, out)
721
844
  end
722
845
 
846
+ # A body whose direct statements certainly run when it does ({#definer_table}): its statement list unless the
847
+ # body has a `rescue` clause, and its `ensure` clause. A `rescue` or `else` clause may not run.
848
+ def mark_certain_body(body)
849
+ return @certain_bodies << body unless body.is_a?(Prism::BeginNode)
850
+
851
+ # With a `rescue`, a statement before a definer can raise and skip it, so the main list is not certain.
852
+ @certain_bodies << body.statements if body.statements && body.rescue_clause.nil?
853
+ @certain_bodies << body.ensure_clause.statements if body.ensure_clause&.statements
854
+ end
855
+
723
856
  # The directive's reach is {Inference::ModuleFunctionState.each_sig_gen_statement}.
724
857
  def walk_statements(stmts_node, prefix, in_singleton_class, module_function_active, out)
858
+ certain_body = @certain_bodies.include?(stmts_node)
725
859
  Inference::ModuleFunctionState.each_sig_gen_statement(stmts_node.body,
726
860
  module_function_active) do |stmt, active|
861
+ if certain_body
862
+ @certain_definers << stmt
863
+ # A `begin … end` statement's body (and `ensure`) runs whenever the statement does.
864
+ mark_certain_body(stmt) if stmt.is_a?(Prism::BeginNode)
865
+ end
727
866
  walk_defs(stmt, prefix, in_singleton_class, active, out)
728
867
  end
729
868
  end
@@ -740,7 +879,7 @@ module Rigor
740
879
  kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
741
880
  class_name = prefix.join("::")
742
881
  if Inference::ModuleFunctionState.sig_gen_module_function?(module_function_active, kind)
743
- @module_function_methods << [class_name, node.name]
882
+ @module_function_methods << node
744
883
  elsif Inference::ModuleFunctionState.sig_gen_uncertain?(module_function_active, kind)
745
884
  @uncertain_module_function_defs << node
746
885
  end
@@ -782,9 +921,9 @@ module Rigor
782
921
 
783
922
  # Returns "def self." (kind: :singleton), "def self?." (instance method declared inside a
784
923
  # `module_function` region — both instance + singleton dispatch at runtime), or "def " (plain instance).
785
- def method_def_prefix(class_name, method_name, kind)
924
+ def method_def_prefix(def_node, kind)
786
925
  return "def self." if kind == :singleton
787
- return "def self?." if @module_function_methods.include?([class_name, method_name])
926
+ return "def self?." if @module_function_methods.include?(def_node)
788
927
 
789
928
  "def "
790
929
  end
@@ -800,7 +939,7 @@ module Rigor
800
939
  def visibility_excludes?(def_node, class_name, kind, scope_index)
801
940
  return false if kind == :singleton
802
941
  return false if @include_private
803
- return false if @module_function_methods.include?([class_name, def_node.name])
942
+ return false if @module_function_methods.include?(def_node)
804
943
 
805
944
  scope = scope_index[def_node] || scope_index.each_value.first
806
945
  return false if scope.nil?
@@ -1038,7 +1177,7 @@ module Rigor
1038
1177
 
1039
1178
  scope = scope_index[def_node]
1040
1179
  method_def = lookup_existing_method(class_name, def_node.name, kind, scope&.environment, scope)
1041
- head = "#{method_def_prefix(class_name, def_node.name, kind)}#{def_node.name}: "
1180
+ head = "#{method_def_prefix(def_node, kind)}#{def_node.name}: "
1042
1181
  if return_defaulted
1043
1182
  return mixed_provenance_candidate(path, def_node, class_name, kind, scope_index, inline, method_def, head)
1044
1183
  end
@@ -1258,7 +1397,37 @@ module Rigor
1258
1397
  # different method (issue #1382). Delegates to {Rigor::Inference::DefReturnTyper} — the same body-typing +
1259
1398
  # explicit-return-union the `rigor annotate` def-line annotator uses.
1260
1399
  def infer_return_type(def_node, scope_index)
1261
- Inference::DefReturnTyper.call(def_node, scope_index)
1400
+ absorb_empty_literals(Inference::DefReturnTyper.call(def_node, scope_index, keep_lookup_nil: true))
1401
+ end
1402
+
1403
+ # Issue #1153 — an empty-collection early return (`return [] if table.empty?`) types its arm `[]`, which the
1404
+ # engine's union keeps beside a sibling `Array[E]` arm on purpose (the empty tuple is a distinct shape). A
1405
+ # signature has no use for the distinction: `[]` is an `Array[E]` for every `E`, so `Array[E] | []` is just
1406
+ # `Array[E]`, and the leftover `[]` arm made the proposal read as if the empty literal pinned the element.
1407
+ # Issue #1149 — the same holds for the empty closed hash shape `{}` (a frozen empty fallback,
1408
+ # `table[key] || EMPTY`) beside a `Hash[K, V]` nominal. Each empty literal is absorbed only beside its OWN
1409
+ # nominal: a `[]` beside a `Hash` arm, or a `{}` beside an `Array` arm, is untouched. A lone empty literal,
1410
+ # one beside another tuple / shape (`[1, 2] | []`), an open `{ ... }` shape, and a non-empty shape are
1411
+ # untouched too, since each of those is information the declaration should keep.
1412
+ def absorb_empty_literals(type)
1413
+ return type unless type.is_a?(Type::Union)
1414
+
1415
+ kept = type.members.reject { |member| absorbed_empty_literal?(member, type.members) }
1416
+ kept.size == type.members.size ? type : Type::Combinator.union(*kept)
1417
+ end
1418
+
1419
+ def absorbed_empty_literal?(member, members)
1420
+ if member.is_a?(Type::Tuple) && member.elements.empty?
1421
+ nominal_arm?(members, "Array")
1422
+ elsif member.is_a?(Type::HashShape) && member.pairs.empty? && member.closed?
1423
+ nominal_arm?(members, "Hash")
1424
+ else
1425
+ false
1426
+ end
1427
+ end
1428
+
1429
+ def nominal_arm?(members, class_name)
1430
+ members.any? { |member| member.is_a?(Type::Nominal) && member.class_name == class_name }
1262
1431
  end
1263
1432
 
1264
1433
  def dynamic_top?(type)
@@ -1305,7 +1474,7 @@ module Rigor
1305
1474
  return equivalent(path, def_node, class_name, kind, inferred, VOID_RETURN_RBS) if declares_void?(method_def)
1306
1475
 
1307
1476
  declared = build_declared_return(method_def)
1308
- if declared_untyped?(declared)
1477
+ if declared_untyped?(declared, method_def)
1309
1478
  return declared_untyped_candidate(path, def_node, class_name, kind, inferred, method_def, rendered: rendered)
1310
1479
  end
1311
1480
 
@@ -1372,7 +1541,7 @@ module Rigor
1372
1541
  returned = paren_wrap_union(elaborated_rbs(inferred, owner: class_name))
1373
1542
  annotations = overload.annotations.map { |a| "#{a.location&.source || "%a{#{a.string}}"} " }.join
1374
1543
  visibility = member.respond_to?(:visibility) && member.visibility ? "#{member.visibility} " : ""
1375
- "#{visibility}#{method_def_prefix(class_name, def_node.name, kind)}#{def_node.name}: " \
1544
+ "#{visibility}#{method_def_prefix(def_node, kind)}#{def_node.name}: " \
1376
1545
  "#{annotations}#{spell_with_return(overload.method_type, returned)}"
1377
1546
  end
1378
1547
 
@@ -1431,8 +1600,13 @@ module Rigor
1431
1600
  # skips. ADR-93's inline `# @rbs param: Type` annotation (no return clause) synthesizes exactly this
1432
1601
  # spelling for every method it documents, so the silent drop hit the parameter-annotated, return-bare
1433
1602
  # shape ADR-14 exists to help.
1434
- def declared_untyped?(declared)
1435
- declared.is_a?(Type::Dynamic)
1603
+ #
1604
+ # The translation alone cannot say the author wrote `untyped`: an interface return (`-> _Taker[1]`,
1605
+ # `-> _ToS`) also translates to `Dynamic`, and reading it as an absent declaration replaced it with an
1606
+ # unrelated nominal under `--overwrite` (#2178). So every overload's return must be `untyped` as written.
1607
+ def declared_untyped?(declared, method_def)
1608
+ declared.is_a?(Type::Dynamic) &&
1609
+ method_def.method_types.all? { |mt| mt.type.return_type.is_a?(RBS::Types::Bases::Any) }
1436
1610
  end
1437
1611
 
1438
1612
  # Proposed without {#tighter?}: a declared `untyped` return carries no information to weigh it against, the
@@ -1465,7 +1639,16 @@ module Rigor
1465
1639
  VALUE_LITERAL_CLASSES = [Integer, String, Symbol].freeze
1466
1640
  private_constant :VALUE_LITERAL_CLASSES
1467
1641
 
1468
- # The same object back when nothing needed widening.
1642
+ # Core generics whose RBS parameters are all `out` or `unchecked out`, so a widened argument is a supertype of
1643
+ # the body's type. An invariant or `in` parameter (a user `class Sink[in T]`) is not widened: `Sink[Integer]`
1644
+ # is no supertype of `Sink[1]` there.
1645
+ COVARIANT_CARRIERS = %w[Array Hash Set Range Enumerator].freeze
1646
+ private_constant :COVARIANT_CARRIERS
1647
+
1648
+ # The same object back when nothing needed widening. A literal nested in a tuple element, a record or hash
1649
+ # value, or a covariant generic argument (`[1, "a"]`, `{ a: 1 }`, `Hash[String, 1]`) widens the same way, while
1650
+ # the carrier's shape and a record's keys stay: a subclass override returning `[2, "b"]` is no widening of
1651
+ # `[Integer, String]`. A nested `nil` is an honest member there, as in a top-level union.
1469
1652
  def widen_value_literals(type)
1470
1653
  case type
1471
1654
  when Type::Constant
@@ -1473,14 +1656,35 @@ module Rigor
1473
1656
  return type unless VALUE_LITERAL_CLASSES.any? { |klass| type.value.instance_of?(klass) }
1474
1657
 
1475
1658
  Type::Combinator.nominal_of(type.value.class.name)
1476
- when Type::Union
1477
- members = type.members.map { |member| widen_value_literals(member) }
1478
- members.each_with_index.all? { |m, i| m.equal?(type.members[i]) } ? type : Type::Combinator.union(*members)
1659
+ when Type::Union then widen_members(type.members, type) { |members| Type::Combinator.union(*members) }
1660
+ when Type::Tuple then widen_members(type.elements, type) { |elements| Type::Combinator.tuple_of(*elements) }
1661
+ when Type::HashShape then widen_hash_shape_values(type)
1662
+ when Type::Nominal
1663
+ return type unless COVARIANT_CARRIERS.include?(type.class_name)
1664
+
1665
+ widen_members(type.type_args, type) do |args|
1666
+ Type::Combinator.nominal_of(type.class_name, type_args: args)
1667
+ end
1479
1668
  else
1480
1669
  type
1481
1670
  end
1482
1671
  end
1483
1672
 
1673
+ def widen_members(members, type)
1674
+ widened = members.map { |member| widen_value_literals(member) }
1675
+ widened.each_with_index.all? { |m, i| m.equal?(members[i]) } ? type : yield(widened)
1676
+ end
1677
+
1678
+ def widen_hash_shape_values(shape)
1679
+ widen_members(shape.pairs.values, shape) do |values|
1680
+ Type::HashShape.new(
1681
+ shape.pairs.keys.zip(values).to_h,
1682
+ required_keys: shape.required_keys, optional_keys: shape.optional_keys,
1683
+ read_only_keys: shape.read_only_keys, extra_keys: shape.extra_keys
1684
+ )
1685
+ end
1686
+ end
1687
+
1484
1688
  def widen_boolean_literal
1485
1689
  Type::Combinator.union(Type::Combinator.constant_of(true), Type::Combinator.constant_of(false))
1486
1690
  end
@@ -1624,11 +1828,12 @@ module Rigor
1624
1828
  # `Type::Constant#erase_to_rbs` spells the values RBS writes as literal types (`true` / `false` /
1625
1829
  # `nil`, an Integer, a Symbol, a String) as that literal and every other carrier as a nominal. Asking
1626
1830
  # which of the two branches it took keeps the list of literal-spellable classes from forking a second
1627
- # copy into this file. A Range's nominal carries its element type (`Range[Integer]`, #1891), so it is
1628
- # excluded by class rather than by comparing the erasure with the class name: like a Float it is a
1629
- # nominal, not a pinned literal.
1831
+ # copy into this file. A Range's and a Set's nominal carries a type argument (`Range[Integer]`, #1891;
1832
+ # `Set[untyped]`, #2177), so they are excluded by class rather than by comparing the erasure with the class
1833
+ # name: like a Float each is a nominal, not a pinned literal.
1630
1834
  def rbs_literal?(type)
1631
- type.is_a?(Type::Constant) && !type.value.is_a?(Range) && type.erase_to_rbs != type.value.class.name
1835
+ type.is_a?(Type::Constant) && !type.value.is_a?(Range) && !type.value.is_a?(::Set) &&
1836
+ type.erase_to_rbs != type.value.class.name
1632
1837
  end
1633
1838
 
1634
1839
  # Heuristic added after the third-round self-dogfood: `FallbackTracer#size` body is `@events.size`, where
@@ -1696,7 +1901,7 @@ module Rigor
1696
1901
  def render_rbs_line(def_node, inferred, class_name, kind)
1697
1902
  params = def_node.parameters
1698
1903
  head = "(#{render_param_list(params, class_name, def_node.name)})#{block_signature_suffix(params)}"
1699
- prefix = method_def_prefix(class_name, def_node.name, kind)
1904
+ prefix = method_def_prefix(def_node, kind)
1700
1905
  "#{prefix}#{def_node.name}: #{head} -> #{paren_wrap_union(elaborated_rbs(inferred, owner: class_name))}"
1701
1906
  end
1702
1907
 
@@ -1800,12 +2005,17 @@ module Rigor
1800
2005
  # Per-file context the attr_* walker threads through its recursive descent. Keeps parameter lists in
1801
2006
  # check. `obs_ivar_map` carries the observation-derived fallback types built by
1802
2007
  # {#build_observed_ivar_map}; it is empty when sig-gen is invoked without `--params=observed`.
1803
- AttrWalkContext = Struct.new(:path, :scope_index, :obs_ivar_map, :out, keyword_init: true)
2008
+ # `definer_table` is {#definer_table}'s map, which drops an attribute another definer replaces. A context
2009
+ # with `definers` only records each attribute's slot and call node for that map, and builds nothing.
2010
+ AttrWalkContext = Struct.new(:path, :scope_index, :obs_ivar_map, :out, :definer_table, :definers,
2011
+ keyword_init: true)
1804
2012
  private_constant :AttrWalkContext
1805
2013
 
1806
- def collect_attr_candidates(root, path, scope_index, obs_ivar_map = {})
2014
+ def collect_attr_candidates(root, path, scope_index, obs_ivar_map = {}, definer_table = {})
2015
+ @lookup_ivars = Set.new
2016
+ collect_lookup_ivar_writes(root, scope_index)
1807
2017
  ctx = AttrWalkContext.new(path: path, scope_index: scope_index,
1808
- obs_ivar_map: obs_ivar_map, out: [])
2018
+ obs_ivar_map: obs_ivar_map, out: [], definer_table: definer_table)
1809
2019
  walk_attr_calls(root, [], false, ctx)
1810
2020
  ctx.out
1811
2021
  end
@@ -1850,11 +2060,67 @@ module Rigor
1850
2060
  class_name = prefix.join("::")
1851
2061
  symbol_names = extract_symbol_arguments(call_node)
1852
2062
  return if symbol_names.empty?
2063
+ return record_attr_definers(call_node, class_name, symbol_names, ctx) if ctx.definers
1853
2064
 
1854
2065
  ivar_lookup = ivar_type_lookup(ctx.scope_index, class_name, ctx.obs_ivar_map)
2066
+ offset = call_node.location.start_offset
1855
2067
  symbol_names.each do |attr_name|
1856
- ivar_type = ivar_lookup.call(attr_name)
1857
- ctx.out.concat(build_attr_candidates(call_node.name, class_name, attr_name, ivar_type, ctx))
2068
+ ivar_type = with_lookup_nil(ivar_lookup.call(attr_name), class_name, attr_name)
2069
+ candidates = build_attr_candidates(call_node.name, class_name, attr_name, ivar_type, ctx)
2070
+ ctx.out.concat(candidates.select do |c|
2071
+ live_definer?(ctx.definer_table, [class_name, :instance, c.method_name], offset)
2072
+ end)
2073
+ end
2074
+ end
2075
+
2076
+ # #2197 — an attribute whose ivar an instance method assigns from an optimistic lookup (`@x = h[k]`) reads
2077
+ # `nil` on a miss, as such a return does ({Inference::DefReturnTyper.optimistic_value?}), so the written
2078
+ # reader keeps the `nil` the engine's nil-free reading of the lookup drops.
2079
+ def with_lookup_nil(type, class_name, attr_name)
2080
+ return type if type.nil? || !@lookup_ivars.include?([class_name, :"@#{attr_name}"])
2081
+
2082
+ Type::Combinator.union(type, Type::Combinator.constant_of(nil))
2083
+ end
2084
+
2085
+ LOOKUP_IVAR_WRITES = [
2086
+ Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode
2087
+ ].freeze
2088
+ private_constant :LOOKUP_IVAR_WRITES
2089
+
2090
+ # Records `[class_name, ivar]` for each ivar an instance `def` assigns an optimistic lookup to. The class
2091
+ # prefixes come from {#collect_other_ivar_writes}' walk, whose own result is not needed here.
2092
+ def collect_lookup_ivar_writes(root, scope_index)
2093
+ collect_other_ivar_writes(root, [], false, Hash.new { |h, k| h[k] = Set.new }) do |def_node, prefix|
2094
+ next if def_node.receiver || prefix.empty?
2095
+
2096
+ scan_lookup_ivar_writes(def_node.body, prefix, scope_index)
2097
+ end
2098
+ end
2099
+
2100
+ def scan_lookup_ivar_writes(node, prefix, scope_index)
2101
+ return unless node.is_a?(Prism::Node)
2102
+
2103
+ record_lookup_ivar_write(node, prefix, scope_index)
2104
+ node.rigor_each_child { |child| scan_lookup_ivar_writes(child, prefix, scope_index) }
2105
+ end
2106
+
2107
+ def record_lookup_ivar_write(node, prefix, scope_index)
2108
+ return unless LOOKUP_IVAR_WRITES.any? { |klass| node.is_a?(klass) }
2109
+
2110
+ scope = scope_index[node.value] || scope_index[node]
2111
+ return if scope.nil? || !Inference::DefReturnTyper.optimistic_value?(
2112
+ node.value, scope, Inference::DefReturnTyper::LookupContext.new(scope_index, {}.compare_by_identity)
2113
+ )
2114
+
2115
+ @lookup_ivars << [prefix.join("::"), node.name]
2116
+ end
2117
+
2118
+ def record_attr_definers(call_node, class_name, symbol_names, ctx)
2119
+ symbol_names.each do |attr_name|
2120
+ ATTR_KINDS.fetch(call_node.name).each do |variant|
2121
+ method_name = variant == :writer ? :"#{attr_name}=" : attr_name
2122
+ ctx.definers << [[class_name, :instance, method_name], call_node]
2123
+ end
1858
2124
  end
1859
2125
  end
1860
2126
 
@@ -1919,20 +2185,25 @@ module Rigor
1919
2185
  # and every write inside an instance `def` or a block, an `instance_variable_set` on `self` included
1920
2186
  # ({#written_ivar}). `def self.x`, a `class << self` body and a bare class-body write set the class
1921
2187
  # object's ivars and do not count.
1922
- def collect_other_ivar_writes(node, prefix, in_block, other_writes)
2188
+ #
2189
+ # `on_def`, when given, is called with each instance `def` reached and its class prefix, so another
2190
+ # per-class reading of instance-method ivar writes shares this walk ({#collect_lookup_ivar_writes}).
2191
+ def collect_other_ivar_writes(node, prefix, in_block, other_writes, &on_def)
1923
2192
  return unless node.is_a?(Prism::Node)
1924
2193
 
1925
2194
  case node
1926
2195
  when Prism::ClassNode, Prism::ModuleNode
1927
2196
  child_prefix = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
1928
- return collect_other_ivar_writes(node.body, child_prefix, false, other_writes) if child_prefix
2197
+ return collect_other_ivar_writes(node.body, child_prefix, false, other_writes, &on_def) if child_prefix
1929
2198
  when Prism::SingletonClassNode then return
1930
- when Prism::DefNode then return record_def_ivar_writes(node, prefix, other_writes)
2199
+ when Prism::DefNode
2200
+ on_def&.call(node, prefix)
2201
+ return record_def_ivar_writes(node, prefix, other_writes)
1931
2202
  when Prism::BlockNode, Prism::LambdaNode then in_block = true
1932
2203
  end
1933
2204
  record_class_level_write(node, prefix, in_block, other_writes) unless prefix.empty?
1934
2205
 
1935
- node.rigor_each_child { |child| collect_other_ivar_writes(child, prefix, in_block, other_writes) }
2206
+ node.rigor_each_child { |child| collect_other_ivar_writes(child, prefix, in_block, other_writes, &on_def) }
1936
2207
  end
1937
2208
 
1938
2209
  # The ivars an `attr_writer` / `attr_accessor` on `self` names, or the ivar a write in a block sets.
@@ -98,7 +98,10 @@ module Rigor
98
98
 
99
99
  base_scope = Scope.empty(environment: environment)
100
100
  base_scope = base_scope.with_discovery(base_scope.discovery.with(discovered_classes: discovered_classes))
101
- scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope)
101
+ # Guard: sig-gen never reads a declared ivar type back (#1406). No spec pins this flag here, because
102
+ # it only matters for an RBS-declared class defined in an observed spec file.
103
+ scope_index = Inference::ScopeIndexer.index(parse_result.value, default_scope: base_scope,
104
+ declared_ivar_types: false)
102
105
  bindings = collect_rspec_bindings(parse_result.value, scope_index)
103
106
  @before_ivars = collect_before_ivars(parse_result.value, scope_index)
104
107
 
@@ -7,7 +7,7 @@ module Rigor
7
7
  # Parses what sig-gen is about to emit, before it is emitted.
8
8
  #
9
9
  # Two rendering bugs have shipped RBS that `rbs` itself rejects — a non-identifier record key
10
- # (`{ :"data-contrast" => T }`) and a `&block` constructor parameter rendered before the parens
10
+ # (`{ "data-contrast": T }`) and a `&block` constructor parameter rendered before the parens
11
11
  # (`(**untyped, ?{ (?) -> void })`). Both were found downstream, as a poisoned `sig/` tree: the file is
12
12
  # quarantined, its types vanish, and (for a project adopting `reject-unparseable-signatures`) the build
13
13
  # fails. A generator that can poison its own consumer's build is worth guarding at the source.
@@ -29,9 +29,11 @@ module Rigor
29
29
  # array means "all emittable classifications".
30
30
  # @param observation — `{ paths:, missing: }` under `--params=observed`, else `nil`; JSON carries it
31
31
  # as the top-level `observation` key (#1398).
32
- def render(candidates:, mode:, format:, selection:, observation: nil)
32
+ # @param diagnostics — the path errors `rigor check` would report for the arguments (#1991); JSON carries
33
+ # them as the top-level `diagnostics` key, and only when there are some.
34
+ def render(candidates:, mode:, format:, selection:, observation: nil, diagnostics: [])
33
35
  case format
34
- when "json" then render_json(filter(candidates, selection, with_skipped: true), observation)
36
+ when "json" then render_json(filter(candidates, selection, with_skipped: true), observation, diagnostics)
35
37
  when "text"
36
38
  filtered = filter(candidates, selection)
37
39
  mode == :diff ? render_diff(filtered) : render_print(filtered)
@@ -124,9 +126,10 @@ module Rigor
124
126
  end
125
127
  end
126
128
 
127
- def render_json(candidates, observation = nil)
129
+ def render_json(candidates, observation = nil, diagnostics = [])
128
130
  payload = { candidates: candidates.map(&:to_h) }
129
131
  payload[:observation] = observation if observation
132
+ payload[:diagnostics] = diagnostics.map(&:to_h) if diagnostics.any?
130
133
  @out.puts(JSON.pretty_generate(payload))
131
134
  end
132
135
 
@@ -37,7 +37,17 @@ module Rigor
37
37
  end
38
38
  end
39
39
 
40
+ # A set is mutable, so the classes the engine reads off its construction (`Set[x]`, `Set.new(xs)`) are not
41
+ # a contract: `x = Set[1]; x << "a"; x` holds a String. A written `Set` therefore keeps `untyped` for its
42
+ # element, as a Set constant does (#2177), however precisely the engine typed it.
43
+ SET_ELEMENT_UNTYPED = "Set"
44
+ private_constant :SET_ELEMENT_UNTYPED
45
+
40
46
  def self.elaborate_nominal(type, environment, arity_cache)
47
+ if type.class_name == SET_ELEMENT_UNTYPED && type.type_args.size == 1
48
+ return Type::Combinator.nominal_of(type.class_name, type_args: [Type::Combinator.untyped])
49
+ end
50
+
41
51
  elaborated_args = type.type_args.map { |arg| walk(arg, environment, arity_cache) }
42
52
  return Type::Combinator.nominal_of(type.class_name, type_args: elaborated_args) if elaborated_args.any?
43
53
 
@@ -57,11 +67,17 @@ module Rigor
57
67
  Type::Tuple.new(elements)
58
68
  end
59
69
 
60
- def self.elaborate_hash_shape(type, _environment, _arity_cache)
61
- # HashShape's element types are read-only on the carrier; rebuilding them would need going through the
62
- # per-pair required/optional/read-only machinery. Sig-gen only routes top-level returns through here for
63
- # now — nested HashShape elaboration ships as a follow-up if the need surfaces.
64
- type
70
+ # A bare generic in a value position (`{ k: Hash.new }`) fails `rbs validate` as a top-level one does
71
+ # (#2177), so the values are elaborated too. Only the pairs change; the key policy is carried over.
72
+ def self.elaborate_hash_shape(type, environment, arity_cache)
73
+ pairs = type.pairs.transform_values { |value| walk(value, environment, arity_cache) }
74
+ return type if pairs == type.pairs
75
+
76
+ Type::HashShape.new(
77
+ pairs,
78
+ required_keys: type.required_keys, optional_keys: type.optional_keys,
79
+ read_only_keys: type.read_only_keys, extra_keys: type.extra_keys
80
+ )
65
81
  end
66
82
 
67
83
  def self.generic_arity_for(class_name, environment, cache)