rigortype 0.3.1 → 0.3.3

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 +10 -10
  3. data/data/core_overlay/pathname.rbs +5 -0
  4. data/data/core_overlay/resolv.rbs +31 -0
  5. data/data/core_overlay/string_scanner.rbs +5 -0
  6. data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
  8. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
  9. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
  10. data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
  11. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
  12. data/docs/manual/02-cli-reference.md +71 -11
  13. data/docs/manual/03-configuration.md +16 -1
  14. data/docs/manual/04-diagnostics.md +8 -0
  15. data/docs/manual/12-caching.md +17 -5
  16. data/docs/manual/15-type-protection-coverage.md +122 -1
  17. data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
  19. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
  20. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
  21. data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
  22. data/lib/rigor/analysis/check_rules.rb +107 -28
  23. data/lib/rigor/analysis/incremental_session.rb +156 -11
  24. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
  25. data/lib/rigor/analysis/rule_catalog.rb +5 -1
  26. data/lib/rigor/analysis/run_cache_key.rb +17 -1
  27. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
  28. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
  29. data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
  30. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
  31. data/lib/rigor/analysis/runner.rb +78 -7
  32. data/lib/rigor/analysis/severity_stamp.rb +2 -1
  33. data/lib/rigor/bleeding_edge.rb +132 -7
  34. data/lib/rigor/cache/descriptor.rb +6 -1
  35. data/lib/rigor/cache/engine_source.rb +162 -0
  36. data/lib/rigor/cache/incremental_snapshot.rb +87 -18
  37. data/lib/rigor/cli/check_command.rb +49 -28
  38. data/lib/rigor/cli/coverage_command.rb +15 -2
  39. data/lib/rigor/cli/coverage_mutation.rb +242 -11
  40. data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
  41. data/lib/rigor/cli/fused_protection_report.rb +12 -3
  42. data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
  43. data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
  44. data/lib/rigor/cli/mutation_protection_report.rb +26 -5
  45. data/lib/rigor/cli/protection_renderer.rb +13 -0
  46. data/lib/rigor/cli/protection_report.rb +11 -3
  47. data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
  48. data/lib/rigor/cli/sig_gen_command.rb +5 -4
  49. data/lib/rigor/configuration.rb +45 -6
  50. data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
  51. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  52. data/lib/rigor/environment/rbs_loader.rb +434 -75
  53. data/lib/rigor/environment.rb +28 -13
  54. data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
  55. data/lib/rigor/inference/expression_typer.rb +62 -4
  56. data/lib/rigor/inference/fork_map.rb +47 -4
  57. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
  58. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  59. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
  60. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +158 -12
  61. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
  62. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +234 -11
  63. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
  64. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
  65. data/lib/rigor/inference/method_dispatcher.rb +41 -10
  66. data/lib/rigor/inference/mutation_widening.rb +30 -25
  67. data/lib/rigor/inference/narrowing.rb +3 -1
  68. data/lib/rigor/inference/optimistic_origin.rb +123 -0
  69. data/lib/rigor/inference/protection_scanner.rb +18 -2
  70. data/lib/rigor/inference/receiver_alias.rb +57 -0
  71. data/lib/rigor/inference/scope_indexer.rb +197 -55
  72. data/lib/rigor/inference/singleton_object_constant.rb +63 -0
  73. data/lib/rigor/inference/statement_evaluator.rb +55 -4
  74. data/lib/rigor/language_server/buffer_table.rb +22 -0
  75. data/lib/rigor/language_server/diagnostic_publisher.rb +203 -4
  76. data/lib/rigor/language_server/project_context.rb +45 -0
  77. data/lib/rigor/language_server/publish_batcher.rb +82 -0
  78. data/lib/rigor/language_server/server.rb +20 -1
  79. data/lib/rigor/language_server.rb +1 -0
  80. data/lib/rigor/plugin/node_rule_walk.rb +6 -0
  81. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  82. data/lib/rigor/protection/closure_kill_oracle.rb +190 -0
  83. data/lib/rigor/protection/dependency_closure.rb +59 -0
  84. data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
  85. data/lib/rigor/protection/discovery_seed.rb +116 -0
  86. data/lib/rigor/protection/kill_signature.rb +31 -0
  87. data/lib/rigor/protection/mutation_cache.rb +355 -0
  88. data/lib/rigor/protection/mutation_scanner.rb +55 -11
  89. data/lib/rigor/protection/mutator.rb +26 -4
  90. data/lib/rigor/runtime/jit.rb +63 -1
  91. data/lib/rigor/scope/discovery_index.rb +9 -0
  92. data/lib/rigor/scope.rb +114 -10
  93. data/lib/rigor/sig_gen/generator.rb +130 -31
  94. data/lib/rigor/sig_gen/layout_index.rb +6 -0
  95. data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
  96. data/lib/rigor/sig_gen/renderer.rb +20 -1
  97. data/lib/rigor/sig_gen/write_result.rb +6 -4
  98. data/lib/rigor/sig_gen/writer.rb +12 -0
  99. data/lib/rigor/sig_gen.rb +1 -0
  100. data/lib/rigor/source/node_walker.rb +12 -0
  101. data/lib/rigor/type/anonymous_class_name.rb +40 -0
  102. data/lib/rigor/type/nominal.rb +5 -2
  103. data/lib/rigor/type/singleton.rb +4 -1
  104. data/lib/rigor/version.rb +1 -1
  105. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
  106. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
  107. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
  108. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
  109. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
  110. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
  111. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
  112. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
  113. data/sig/rigor/inference/void_origin.rbs +6 -3
  114. data/sig/rigor/scope.rbs +12 -0
  115. data/sig/rigor.rbs +4 -0
  116. metadata +21 -1
@@ -18,7 +18,8 @@ module Rigor
18
18
  #
19
19
  # Catalogue (Slice 5 phase 2):
20
20
  #
21
- # - Tuple#`first`, Tuple#`last`, Tuple#`size`/`length`/`count`: no-arg, no-block.
21
+ # - Tuple#`first`, Tuple#`last` (no-arg, or a single `Constant[Integer]` count lifting to a
22
+ # sub-`Tuple`, the `take`/`drop` shape), Tuple#`size`/`length`/`count`: no-block.
22
23
  # - Tuple#`[]`, Tuple#`fetch` with a single `Constant[Integer]` argument inside the tuple's bounds
23
24
  # (negative indices are normalised by length). Tuple#`[]` also handles static Range and
24
25
  # start-length slices, returning a sliced Tuple or `Constant[nil]` for statically nil slices.
@@ -76,25 +77,48 @@ module Rigor
76
77
  fetch: :tuple_index,
77
78
  dig: :tuple_dig,
78
79
  values_at: :tuple_values_at,
80
+ # `concat` / `<<` are deliberately ABSENT from this table: both mutate the receiver in place
81
+ # (`ARRAY_MUTATORS` in `MutationWidening`), and a fold tier has no way to write the folded
82
+ # result back into the caller's binding — it can only return a value for the call expression.
83
+ # `tuple.concat(other)` would fold correctly as a VALUE (same shape as `tuple + other`) but the
84
+ # receiver local/ivar would still carry the pre-concat Tuple afterward, which is unsound (#121).
85
+ # `MutationWidening.widen_after_call` is the actual precision mechanism for a mutator call site:
86
+ # it forgets the literal-arity carrier so a stale `size`/`empty?` fold cannot survive the
87
+ # mutation, rather than trying to fold the mutator itself.
79
88
  :+ => :tuple_concat,
80
89
  compact: :tuple_compact,
81
90
  take: :tuple_take,
82
91
  drop: :tuple_drop,
83
92
  rotate: :tuple_rotate,
84
93
  uniq: :tuple_uniq,
94
+ at: :tuple_at,
95
+ :& => :tuple_intersection,
96
+ intersection: :tuple_intersection,
97
+ :| => :tuple_union,
98
+ union: :tuple_union,
99
+ :- => :tuple_difference,
100
+ difference: :tuple_difference,
101
+ intersect?: :tuple_intersect?,
102
+ one?: :tuple_one?,
103
+ deconstruct: :shape_self,
85
104
  index: :tuple_find_index,
86
105
  find_index: :tuple_find_index,
87
106
  rindex: :tuple_rindex,
88
107
  flatten: :tuple_flatten,
89
108
  join: :tuple_join,
109
+ # `inspect` / `to_s` (an exact alias for Array — `Array#to_s` IS `Array#inspect`) — see
110
+ # {#tuple_inspect}, below the join helpers it shares a byte cap with.
111
+ inspect: :tuple_inspect,
112
+ to_s: :tuple_inspect,
113
+ :* => :tuple_star,
90
114
  freeze: :shape_self,
91
115
  dup: :shape_self,
92
116
  clone: :shape_self,
93
117
  itself: :shape_self
94
118
  }.freeze
95
119
 
96
- # Byte cap on a folded `tuple.join` result — a huge tuple times a long separator must not
97
- # materialise an unbounded `Constant`.
120
+ # Byte cap on a folded `tuple.join` / `tuple.inspect` / `hash.inspect` result — a huge tuple or
121
+ # shape must not materialise an unbounded `Constant`.
98
122
  TUPLE_JOIN_BYTE_LIMIT = 4096
99
123
  private_constant :TUPLE_JOIN_BYTE_LIMIT
100
124
 
@@ -115,6 +139,13 @@ module Rigor
115
139
  entries: :hash_to_a,
116
140
  to_h: :hash_to_h,
117
141
  to_hash: :hash_to_h,
142
+ # `inspect` / `to_s` (an exact alias for Hash — `Hash#to_s` IS `Hash#inspect`) — see
143
+ # {#hash_inspect}. Gated CLOSED-no-optional-key, same predicate `URIFolding#hash_form_pairs`
144
+ # uses for `URI.encode_www_form`'s HashShape argument: an open or partially-optional shape
145
+ # describes a hash whose real membership this fold cannot see, so its `inspect` string is not
146
+ # deterministic.
147
+ inspect: :hash_inspect,
148
+ to_s: :hash_inspect,
118
149
  deconstruct_keys: :hash_deconstruct_keys,
119
150
  invert: :hash_invert,
120
151
  merge: :hash_merge,
@@ -672,23 +703,39 @@ module Rigor
672
703
  Type::Combinator.integer_range(min, max)
673
704
  end
674
705
 
675
- # `first` (no arg) → the first element (or `Constant[nil]` when empty). The `first(n)` arg-form
676
- # is deliberately left to RBS overload selection (see the overload-selection specs) folding it
677
- # here would change that documented `Array[Elem]` contract.
706
+ # `first` (no arg) → the first element (or `Constant[nil]` when empty). `first(n)` → a `Tuple` of
707
+ # the leading `n` elements, the same `take`/`drop` shape via `non_negative_count_arg` (`n >= size`
708
+ # returns the full receiver, a non-static or negative count declines to RBS).
678
709
  def tuple_first(tuple, _method_name, args)
679
- return nil unless args.empty?
710
+ return tuple_first_n(tuple, args) unless args.empty?
680
711
  return Type::Combinator.constant_of(nil) if tuple.elements.empty?
681
712
 
682
713
  tuple.elements.first
683
714
  end
684
715
 
716
+ def tuple_first_n(tuple, args)
717
+ n = non_negative_count_arg(args)
718
+ return nil if n.nil?
719
+
720
+ Type::Combinator.tuple_of(*tuple.elements.first(n))
721
+ end
722
+
723
+ # `last` (no arg) → the last element (or `Constant[nil]` when empty). `last(n)` → a `Tuple` of the
724
+ # trailing `n` elements, mirroring `tuple_first_n`.
685
725
  def tuple_last(tuple, _method_name, args)
686
- return nil unless args.empty?
726
+ return tuple_last_n(tuple, args) unless args.empty?
687
727
  return Type::Combinator.constant_of(nil) if tuple.elements.empty?
688
728
 
689
729
  tuple.elements.last
690
730
  end
691
731
 
732
+ def tuple_last_n(tuple, args)
733
+ n = non_negative_count_arg(args)
734
+ return nil if n.nil?
735
+
736
+ Type::Combinator.tuple_of(*tuple.elements.last(n))
737
+ end
738
+
692
739
  def tuple_size(tuple, _method_name, args)
693
740
  return nil unless args.empty?
694
741
 
@@ -796,6 +843,59 @@ module Rigor
796
843
  arg.value
797
844
  end
798
845
 
846
+ # `tuple.inspect` / `tuple.to_s` (`Array#to_s` is an exact alias of `Array#inspect`) — folds when
847
+ # every element is a `Constant`, by reconstructing the Ruby Array and calling the REAL `inspect`
848
+ # so the analyzer's Ruby (4.0.x's `Array#inspect` format) is matched rather than re-derived.
849
+ # Capped at `TUPLE_JOIN_BYTE_LIMIT` like `#join`, since a large tuple's `inspect` string is
850
+ # similarly unbounded.
851
+ def tuple_inspect(tuple, _method_name, args)
852
+ return nil unless args.empty?
853
+
854
+ values = constant_values(tuple.elements)
855
+ return nil if values.nil?
856
+
857
+ result = values.inspect
858
+ return nil if result.bytesize > TUPLE_JOIN_BYTE_LIMIT
859
+
860
+ Type::Combinator.constant_of(result)
861
+ rescue StandardError
862
+ nil
863
+ end
864
+
865
+ # `tuple * n` — a `Constant[String]` argument is `Array#*`'s join-alias form (identical semantics
866
+ # to `#join`); a `Constant[Integer]` argument is repetition. Any other argument shape (2+ args,
867
+ # `Dynamic`, `Float`, no args) declines so the RBS / alias-pass tier answers — for the Integer
868
+ # case that tier already gives a decent union-of-elements `Array[...]` answer, so a decline here
869
+ # is not a regression.
870
+ def tuple_star(tuple, _method_name, args)
871
+ return nil unless args.size == 1
872
+
873
+ arg = args.first
874
+ return nil unless arg.is_a?(Type::Constant)
875
+
876
+ case arg.value
877
+ when String then tuple_join(tuple, :join, args)
878
+ when Integer then tuple_repeat(tuple, arg.value)
879
+ end
880
+ end
881
+
882
+ # `tuple * n` repetition. `n == 0` folds to the empty Tuple (any array repeated 0 times is `[]`,
883
+ # even for an unbounded/empty receiver). A negative `n` declines — `Array#*` raises
884
+ # `ArgumentError` at runtime, so no fold should invent a value for it (same fail-soft discipline
885
+ # every other handler in this catalogue follows). The result element count is capped at
886
+ # `MAX_SET_OPERATION_SIZE` (64) — the same element-count-cap convention as
887
+ # `ShellwordsFolding::SHELLWORDS_SPLIT_LIMIT` / `RegexpFolding`'s `Regexp.union` cap / the Tuple
888
+ # set operations above, reused rather than minting a second constant with the same value.
889
+ def tuple_repeat(tuple, count)
890
+ return nil if count.negative?
891
+ return Type::Combinator.tuple_of if count.zero?
892
+
893
+ total = tuple.elements.size * count
894
+ return nil if total > MAX_SET_OPERATION_SIZE
895
+
896
+ Type::Combinator.tuple_of(*(tuple.elements * count))
897
+ end
898
+
799
899
  # `tuple.min` / `tuple.max` — fold when every element is a `Constant` whose values share a
800
900
  # Ruby-comparable domain. Empty tuples fold to `Constant[nil]`. The 1-arg `min(n)` / `max(n)`
801
901
  # form folds to a `Tuple` of the n edge-most values in Ruby's order (`min(n)` ascending, `max(n)`
@@ -957,6 +1057,88 @@ module Rigor
957
1057
 
958
1058
  # `tuple + other` — concatenates two Tuples. Both sides must be `Type::Tuple`. Returns a new
959
1059
  # Tuple whose elements are those of the receiver followed by those of the argument.
1060
+ # `at(n)` — the strict single-Integer accessor. Deliberately NOT an alias of {#tuple_index}: `[]`
1061
+ # and `slice` accept a Range or a `(start, length)` pair, while `Array#at` raises `ArgumentError`
1062
+ # on anything but one Integer, and a fold must never invent a value for a call that raises.
1063
+ #
1064
+ # An out-of-range index declines rather than folding to `Constant[nil]`, matching {#tuple_index}.
1065
+ # Ruby really does return nil there, so the fold would be correct — but it would also turn a
1066
+ # receiver the RBS tier types as `Elem?` into a proven nil, newly surfacing diagnostics on code
1067
+ # that may be guarded in ways a shape cannot see. Precision that fires a new diagnostic is a
1068
+ # separate decision from precision that removes a `Dynamic` (#121).
1069
+ def tuple_at(tuple, _method_name, args)
1070
+ return nil unless args.size == 1
1071
+ return nil unless args.first.is_a?(Type::Constant) && args.first.value.is_a?(Integer)
1072
+
1073
+ tuple.elements[args.first.value]
1074
+ end
1075
+
1076
+ # `a & b` / `a.intersection(b, ...)`, `a | b` / `a.union(...)`, `a - b` / `a.difference(...)`.
1077
+ # Every set operation is evaluated by running Ruby's own operator over the unwrapped values, which
1078
+ # is the only way to reproduce its `eql?` / `hash` semantics exactly — `[1] & [1.0]` is empty even
1079
+ # though `1 == 1.0`, so an equality-based reimplementation would fold the wrong answer.
1080
+ def tuple_intersection(tuple, _method_name, args)
1081
+ set_operation(tuple, args) { |values, others| others.reduce(values, :&) }
1082
+ end
1083
+
1084
+ def tuple_union(tuple, _method_name, args)
1085
+ set_operation(tuple, args) { |values, others| others.reduce(values, :|) }
1086
+ end
1087
+
1088
+ def tuple_difference(tuple, _method_name, args)
1089
+ set_operation(tuple, args) { |values, others| others.reduce(values, :-) }
1090
+ end
1091
+
1092
+ # `a.intersect?(b)` — the predicate form, `Constant[bool]`. `nil` is the decline signal every
1093
+ # handler shares, not a boolean answer, so the predicate-return cop is disabled as it is for the
1094
+ # other `?`-named handlers above.
1095
+ # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
1096
+ def tuple_intersect?(tuple, _method_name, args)
1097
+ result = set_operation(tuple, args) { |values, others| others.reduce(values, :&) }
1098
+ return nil if result.nil?
1099
+
1100
+ Type::Combinator.constant_of(!result.elements.empty?)
1101
+ end
1102
+
1103
+ # Shared body for the set operations: unwrap the receiver and every Tuple argument, hand the raw
1104
+ # values to `block`, and lift the result back. Declines unless every element on both sides is a
1105
+ # `Constant` (an unknown element makes membership undecidable) and every argument is a `Tuple` —
1106
+ # `Array#&` also accepts anything that responds to `to_ary`, which a shape cannot prove.
1107
+ def set_operation(tuple, args)
1108
+ return nil if args.empty? || args.size > MAX_SET_OPERATION_ARITY
1109
+
1110
+ values = constant_values(tuple.elements)
1111
+ return nil if values.nil?
1112
+
1113
+ others = args.map do |arg|
1114
+ return nil unless arg.is_a?(Type::Tuple)
1115
+
1116
+ constant_values(arg.elements) || (return nil)
1117
+ end
1118
+ result = yield(values, others)
1119
+ return nil if result.size > MAX_SET_OPERATION_SIZE
1120
+
1121
+ Type::Combinator.tuple_of(*result.map { |value| Type::Combinator.constant_of(value) })
1122
+ end
1123
+
1124
+ # An argument list longer than this, or a result wider than this, declines to the RBS tier rather
1125
+ # than materialising an unbounded Tuple — the same discipline as {MAX_ZIP_ARITY} / the join cap.
1126
+ MAX_SET_OPERATION_ARITY = 8
1127
+ MAX_SET_OPERATION_SIZE = 64
1128
+ private_constant :MAX_SET_OPERATION_ARITY, :MAX_SET_OPERATION_SIZE
1129
+
1130
+ # `one?` with no block and no pattern — `Constant[bool]` of "exactly one truthy element". Only
1131
+ # `Constant` elements have decidable truthiness, and the block / pattern forms defer.
1132
+ def tuple_one?(tuple, _method_name, args)
1133
+ return nil unless args.empty?
1134
+
1135
+ values = constant_values(tuple.elements)
1136
+ return nil if values.nil?
1137
+
1138
+ Type::Combinator.constant_of(values.one? { |value| value })
1139
+ end
1140
+ # rubocop:enable Style/ReturnNilInPredicateMethodDefinition
1141
+
960
1142
  def tuple_concat(tuple, _method_name, args)
961
1143
  return nil unless args.size == 1
962
1144
 
@@ -1487,6 +1669,28 @@ module Rigor
1487
1669
  shape
1488
1670
  end
1489
1671
 
1672
+ # `shape.inspect` / `shape.to_s` (`Hash#to_s` is an exact alias of `Hash#inspect`) — folds only
1673
+ # on a CLOSED shape with no optional keys (an open or partially-optional shape's real membership
1674
+ # is not fully known, so its `inspect` string is not deterministic — the same gate
1675
+ # `URIFolding#hash_form_pairs` uses for `URI.encode_www_form`'s HashShape argument) and only when
1676
+ # every value is a `Constant` ({#constant_pairs}). Reconstructs the Ruby Hash and calls the REAL
1677
+ # `inspect` so the analyzer matches Ruby 4.0.x's `{a: 1}` hash-inspect format rather than
1678
+ # re-deriving it. Capped at `TUPLE_JOIN_BYTE_LIMIT` like the Tuple sibling.
1679
+ def hash_inspect(shape, _method_name, args)
1680
+ return nil unless args.empty?
1681
+ return nil unless shape.closed? && shape.optional_keys.empty?
1682
+
1683
+ pairs = constant_pairs(shape)
1684
+ return nil if pairs.nil?
1685
+
1686
+ result = pairs.inspect
1687
+ return nil if result.bytesize > TUPLE_JOIN_BYTE_LIMIT
1688
+
1689
+ Type::Combinator.constant_of(result)
1690
+ rescue StandardError
1691
+ nil
1692
+ end
1693
+
1490
1694
  # `shape.invert` — swaps keys and values. Folds when every value is a `Constant` whose value is a
1491
1695
  # scalar the carrier accepts as a key (Symbol / String / Integer / Float / true / false / nil;
1492
1696
  # see {#static_shape_key?}). Duplicate values
@@ -1558,13 +1762,16 @@ module Rigor
1558
1762
  Type::Combinator.hash_shape_of(shape.pairs.merge(other.pairs))
1559
1763
  end
1560
1764
 
1561
- # `shape[k]` and `shape.fetch(k)` for a value-pinned scalar key. Missing-key resolution depends
1562
- # on the method:
1765
+ # `shape[k]` and `shape.fetch(k)` for a value-pinned scalar key. On a CLOSED shape a key outside
1766
+ # `pairs` is provably missing, and the resolution depends on the method:
1563
1767
  #
1564
1768
  # - `[]` returns `nil` at runtime; we surface `Constant[nil]` so the carrier is visible to
1565
1769
  # downstream narrowing.
1566
1770
  # - `fetch` (no default, no block) raises `KeyError`; we let the projection answer apply because
1567
1771
  # the runtime would not produce a value.
1772
+ #
1773
+ # On an `:open` shape neither applies — the key is unknown rather than missing, so both methods
1774
+ # answer the `untyped` step from {#hash_dig_step}.
1568
1775
  def hash_lookup(shape, method_name, args)
1569
1776
  return nil unless args.size == 1
1570
1777
 
@@ -1594,6 +1801,12 @@ module Rigor
1594
1801
  # Returns the per-step value type for a HashShape lookup (or `Constant[nil]` for a known-missing
1595
1802
  # key). Returns `nil` when the argument is not a value-pinned scalar key so the caller can fall
1596
1803
  # through to the projection answer.
1804
+ #
1805
+ # A key outside `pairs` only resolves to `Constant[nil]` on a CLOSED shape, where `pairs` is the
1806
+ # whole key set and the runtime read provably returns nil. On an `:open` shape the key set is by
1807
+ # definition not exhausted, so an undeclared key is *unknown*, not absent — it may hold a value
1808
+ # of any type. Typing it `Constant[nil]` there would put `call.undefined-method` on the next line
1809
+ # of correct code (`payload[:undeclared].upcase`), so the open case answers `untyped`.
1597
1810
  def hash_dig_step(shape, arg)
1598
1811
  return nil unless arg.is_a?(Type::Constant)
1599
1812
 
@@ -1607,6 +1820,8 @@ module Rigor
1607
1820
  return Type::Combinator.union(value, Type::Combinator.constant_of(nil))
1608
1821
  end
1609
1822
 
1823
+ return Type::Combinator.untyped if shape.open?
1824
+
1610
1825
  Type::Combinator.constant_of(nil)
1611
1826
  end
1612
1827
 
@@ -1616,8 +1831,13 @@ module Rigor
1616
1831
  shape.optional_key?(arg.value)
1617
1832
  end
1618
1833
 
1834
+ # Whether the key is provably ABSENT — which only a closed shape can establish. An open shape
1835
+ # permits keys outside `pairs`, so a key it does not declare is unknown rather than missing, and
1836
+ # `fetch` must not be deferred on the theory that Ruby would raise `KeyError`: the key may well
1837
+ # be there. {#hash_dig_step} has already answered `untyped` for that case.
1619
1838
  def missing_key_step?(shape, arg)
1620
1839
  return false unless arg.is_a?(Type::Constant)
1840
+ return false if shape.open?
1621
1841
 
1622
1842
  !shape.pairs.key?(arg.value)
1623
1843
  end
@@ -1685,7 +1905,9 @@ module Rigor
1685
1905
 
1686
1906
  # Continues a `dig` chain after the first step. Tuple and HashShape members re-dispatch into the
1687
1907
  # catalogue; `Constant[nil]` short-circuits the chain (Hash#dig and Array#dig do the same at
1688
- # runtime); anything else falls through so the projection answer applies.
1908
+ # runtime); an `untyped` step (an undeclared key on an open shape) keeps the whole chain
1909
+ # `untyped`, since digging further into an unknown value cannot recover a bound; anything else
1910
+ # falls through so the projection answer applies.
1689
1911
  def chain_dig(receiver, args)
1690
1912
  return receiver if args.empty?
1691
1913
 
@@ -1693,6 +1915,7 @@ module Rigor
1693
1915
  when Type::Tuple then tuple_dig(receiver, :dig, args)
1694
1916
  when Type::HashShape then hash_dig(receiver, :dig, args)
1695
1917
  when Type::Constant then receiver.value.nil? ? Type::Combinator.constant_of(nil) : nil
1918
+ when Type::Dynamic then Type::Combinator.untyped
1696
1919
  end
1697
1920
  end
1698
1921
  end
@@ -141,7 +141,7 @@ module Rigor
141
141
  map = member_map_for_new(members, keyword_init, context)
142
142
  return degraded_instance if map.nil?
143
143
 
144
- Type::Combinator.struct_instance_of(members: map, class_name: class_name)
144
+ Type::Combinator.struct_instance_of(members: widen_unowned_emptiness(map), class_name: class_name)
145
145
  end
146
146
 
147
147
  # Builds the member -> type map honouring the class's `keyword_init` flag: a `keyword_init: true`
@@ -309,7 +309,8 @@ module Rigor
309
309
  return scope if member.nil?
310
310
 
311
311
  rebound = Type::Combinator.struct_instance_of(
312
- members: current.members.merge(member => assigned_type), class_name: current.class_name
312
+ members: widen_unowned_emptiness(current.members.merge(member => assigned_type)),
313
+ class_name: current.class_name
313
314
  )
314
315
  scope.with_local(receiver.name, rebound)
315
316
  end
@@ -20,12 +20,31 @@ module Rigor
20
20
  # decode. Returns `Constant[String]`.
21
21
  # * `encode_uri_component(str)` / `decode_uri_component(str)` — Same encoding but may preserve
22
22
  # additional reserved chars (Ruby 3.2+). Returns `Constant[String]`.
23
+ # * `encode_www_form(enum)` — the whole-form encoder, over a `Tuple` of `[key, value]` `Tuple`s (an
24
+ # array literal) or a `HashShape` (a hash literal) whose keys and values are all `Constant`.
25
+ # Returns `Constant[String]`.
26
+ # * `decode_www_form(str)` — the inverse, over a `Constant[String]`. Returns a `Tuple` of two-element
27
+ # `Tuple[Constant[String], Constant[String]]` pairs rather than the RBS tier's
28
+ # `Array[[String, String]]`, so a destructuring read of a known form gets the concrete strings.
29
+ # * `extract(str)` — the URIs a constant string contains, as a `Tuple` of `Constant[String]` rather
30
+ # than `Array[String]`. Single-argument form only (see {.fold_extract}).
31
+ #
32
+ # === Deliberately NOT folded
33
+ #
34
+ # `URI.parse` and `URI.join` are the two remaining gaps in this module's coverage note, and both are
35
+ # declined on the repo's own existing rule rather than on judgement: they return `URI::Generic` and
36
+ # friends, which are absent from `ConstantFolding::FOLDABLE_CONSTANT_CLASSES`, so there is no
37
+ # `Constant[…]` for this tier to produce. Narrowing `URI.parse`'s ten-arm return union to the one
38
+ # scheme class a constant string selects WOULD be a real precision win, but it makes a gradually-valid
39
+ # dispatch precise and can therefore surface a diagnostic that does not fire today — which puts it in
40
+ # the bucket-3 / P0 category, not in this FP-safe fold category ([#121](
41
+ # https://github.com/rigortype/rigor/issues/121)).
23
42
  #
24
43
  # === Non-constant / unsupported cases
25
44
  #
26
45
  # Returns `nil` (deferring to the next dispatcher tier) when:
27
46
  # - the receiver is not `Singleton[URI]`,
28
- # - the first argument is not a `Constant[String]`,
47
+ # - an argument is not the constant shape the method's fold requires,
29
48
  # - the method is not in the supported set.
30
49
  module URIFolding
31
50
  URI_COMPONENT_METHODS = Set[
@@ -34,6 +53,12 @@ module Rigor
34
53
  ].freeze
35
54
  private_constant :URI_COMPONENT_METHODS
36
55
 
56
+ # A form long enough to suggest generated or accumulated data rather than a literal a reader wrote;
57
+ # past it the folded `Constant[String]` stops being useful in a diagnostic and only costs memory.
58
+ # Mirrors the byte ceiling `ShapeDispatch`'s own `tuple_join` fold applies for the same reason.
59
+ FORM_PAIR_LIMIT = 64
60
+ private_constant :FORM_PAIR_LIMIT
61
+
37
62
  module_function
38
63
 
39
64
  # @return [Rigor::Type, nil] folded result, or nil to defer.
@@ -42,11 +67,109 @@ module Rigor
42
67
  method_name = context.method_name
43
68
  args = context.args
44
69
  return nil unless SingletonFolding.receiver?(receiver, "URI")
70
+ return fold_encode_www_form(args) if method_name == :encode_www_form
71
+ return fold_decode_www_form(args) if method_name == :decode_www_form
72
+ return fold_extract(args) if method_name == :extract
45
73
  return nil unless URI_COMPONENT_METHODS.include?(method_name)
46
74
 
47
75
  fold_uri_call(method_name, args)
48
76
  end
49
77
 
78
+ # `URI.encode_www_form(enum)` — deterministic over its whole input, so it folds only when EVERY key
79
+ # and value is already a `Constant`. One non-constant pair declines the whole call: a partially
80
+ # known form has no `Constant[String]` to stand for it.
81
+ def fold_encode_www_form(args)
82
+ return nil unless args.size == 1
83
+
84
+ pairs = form_pairs(args.first)
85
+ return nil if pairs.nil? || pairs.size > FORM_PAIR_LIMIT
86
+
87
+ Type::Combinator.constant_of(URI.encode_www_form(pairs))
88
+ rescue StandardError
89
+ nil
90
+ end
91
+
92
+ # The `[key, value]` pairs of a literal form argument, or nil when the shape is not fully known:
93
+ # a `Tuple` of two-element `Tuple`s (the array-of-pairs spelling) or a `HashShape` (the hash
94
+ # spelling). `HashShape` pairs are read only when the shape is CLOSED and carries no optional key —
95
+ # an open or partially-optional shape describes a hash whose real contents this fold cannot see.
96
+ def form_pairs(arg)
97
+ case arg
98
+ when Type::Tuple then tuple_form_pairs(arg)
99
+ when Type::HashShape then hash_form_pairs(arg)
100
+ end
101
+ end
102
+
103
+ def tuple_form_pairs(tuple)
104
+ tuple.elements.map do |element|
105
+ return nil unless element.is_a?(Type::Tuple) && element.elements.size == 2
106
+ return nil unless element.elements.all?(Type::Constant)
107
+
108
+ element.elements.map(&:value)
109
+ end
110
+ end
111
+
112
+ def hash_form_pairs(shape)
113
+ return nil unless shape.closed? && shape.optional_keys.empty?
114
+
115
+ shape.pairs.map do |key, value|
116
+ return nil unless value.is_a?(Type::Constant)
117
+
118
+ [key, value.value]
119
+ end
120
+ end
121
+
122
+ # `URI.decode_www_form(str)` — the pair list of a constant form, as a `Tuple` of two-element
123
+ # `Tuple`s so a destructuring read (`k, v = URI.decode_www_form(s).first`) reaches the concrete
124
+ # strings instead of the RBS tier's `Array[[String, String]]`.
125
+ def fold_decode_www_form(args)
126
+ return nil unless args.size == 1
127
+
128
+ str = SingletonFolding.constant_string(args.first)
129
+ return nil if str.nil?
130
+
131
+ pairs = URI.decode_www_form(str)
132
+ return nil if pairs.size > FORM_PAIR_LIMIT
133
+
134
+ Type::Combinator.tuple_of(*pairs.map { |key, value| constant_pair(key, value) })
135
+ rescue StandardError
136
+ nil
137
+ end
138
+
139
+ def constant_pair(key, value)
140
+ Type::Combinator.tuple_of(Type::Combinator.constant_of(key), Type::Combinator.constant_of(value))
141
+ end
142
+
143
+ # `URI.extract(str)` — the URIs a constant string contains, as a `Tuple` of `Constant[String]`
144
+ # instead of the RBS tier's `Array[String]`. Only the single-argument form folds: the optional
145
+ # second argument is a schema FILTER, and honouring it means reproducing `URI.extract`'s own
146
+ # argument handling rather than the one call this fold makes, so it declines instead.
147
+ #
148
+ # Ruby emits an obsolescence warning for this method under `$VERBOSE`; the fold calls it with
149
+ # warnings silenced, because a warning about the ANALYZED program's API choice must not appear on
150
+ # the analyzer's own stderr, where a reader would attribute it to Rigor.
151
+ def fold_extract(args)
152
+ return nil unless args.size == 1
153
+
154
+ str = SingletonFolding.constant_string(args.first)
155
+ return nil if str.nil?
156
+
157
+ found = silence_warnings { URI.extract(str) }
158
+ return nil if found.nil? || found.size > FORM_PAIR_LIMIT
159
+
160
+ Type::Combinator.tuple_of(*found.map { |uri| Type::Combinator.constant_of(uri) })
161
+ rescue StandardError
162
+ nil
163
+ end
164
+
165
+ def silence_warnings
166
+ previous = $VERBOSE
167
+ $VERBOSE = nil
168
+ yield
169
+ ensure
170
+ $VERBOSE = previous
171
+ end
172
+
50
173
  def fold_uri_call(method_name, args)
51
174
  return nil unless args.size == 1
52
175
 
@@ -6,6 +6,7 @@ require_relative "../flow_contribution"
6
6
  require_relative "../flow_contribution/merger"
7
7
  require_relative "../builtins/hkt_builtins"
8
8
  require_relative "../builtins/static_return_refinements"
9
+ require_relative "anonymous_meta_class"
9
10
  require_relative "dynamic_origin"
10
11
  require_relative "flow_tracer"
11
12
  require_relative "void_tail_summary"
@@ -760,7 +761,7 @@ module Rigor
760
761
  struct_result = StructFolding.try_dispatch(context)
761
762
  return struct_result if struct_result
762
763
 
763
- meta_result = try_meta_introspection(context.receiver, context.method_name, context.args)
764
+ meta_result = try_meta_introspection(context.receiver, context.method_name, context.args, context)
764
765
  return meta_result if meta_result
765
766
 
766
767
  PRECISE_TIERS_HEAD.each do |tier|
@@ -868,10 +869,10 @@ module Rigor
868
869
  # `Foo.class` as `Singleton[Class]` (deliberate; calling `.class` on a class object yields
869
870
  # `Class`, the metaclass). We also special-case `is_a?`-adjacent calls and the trivial
870
871
  # `instance_of?(self)` later as the rule catalogue grows; for now only `class` is handled.
871
- def try_meta_introspection(receiver_type, method_name, arg_types = [])
872
+ def try_meta_introspection(receiver_type, method_name, arg_types = [], context = nil)
872
873
  case method_name
873
874
  when :class then meta_class(receiver_type)
874
- when :new then meta_new(receiver_type, arg_types)
875
+ when :new then meta_new(receiver_type, arg_types, context)
875
876
  end
876
877
  end
877
878
 
@@ -891,13 +892,13 @@ module Rigor
891
892
  # `Type::Constant::SCALAR_CLASSES` accepts (today: `Pathname`), `.new(Constant<…>)` lifts
892
893
  # to a `Constant<…>` carrier so downstream method calls fold through the standard catalog
893
894
  # tier.
894
- def meta_new(receiver_type, arg_types = [])
895
+ def meta_new(receiver_type, arg_types = [], context = nil)
895
896
  return nil unless receiver_type.is_a?(Type::Singleton)
896
897
 
897
898
  constant_lift = constant_constructor_lift(receiver_type.class_name, arg_types)
898
899
  return constant_lift if constant_lift
899
900
 
900
- array_lift = array_new_lift(receiver_type.class_name, arg_types)
901
+ array_lift = array_new_lift(receiver_type.class_name, arg_types, context&.block_type)
901
902
  return array_lift if array_lift
902
903
 
903
904
  range_lift = range_new_lift(receiver_type.class_name, arg_types)
@@ -918,7 +919,7 @@ module Rigor
918
919
  struct_new_lift = struct_new_lift(receiver_type.class_name, arg_types)
919
920
  return struct_new_lift if struct_new_lift
920
921
 
921
- class_new_lift = class_new_lift(receiver_type.class_name, arg_types)
922
+ class_new_lift = class_new_lift(receiver_type.class_name, arg_types, context)
922
923
  return class_new_lift if class_new_lift
923
924
 
924
925
  Type::Combinator.nominal_of(receiver_type.class_name)
@@ -955,9 +956,22 @@ module Rigor
955
956
  # so `Singleton[Parent]` lets downstream `klass.some_class_method` resolve. No parent →
956
957
  # `singleton(Object)`. Anything else (dynamic parent, more than one positional, …) falls
957
958
  # back to `Nominal[Class]` via the surrounding `meta_new` tail.
958
- def class_new_lift(class_name, arg_types)
959
+ # #319 — a `Class.new do ... end` with no parent is NOT `Object`: the block body is a class body, so the
960
+ # class owns whatever that body defines. Answering `Singleton[Object]` handed the call site `Object`'s
961
+ # zero-arity `new` and `Object`'s method surface, so an `initialize(bucket)` written right there produced
962
+ # `wrong number of arguments ... (given 1, expected 0)` on code Ruby runs happily. `ScopeIndexer` registers
963
+ # the body's methods under the call site's anonymous name; return that name so the lookups reach them.
964
+ #
965
+ # Scoped to the parentless form on purpose. `Class.new(Parent) { ... }` keeps `Singleton[Parent]`: the
966
+ # anonymous name would have to carry the full ancestor chain for `Parent`'s own surface to stay reachable,
967
+ # and the parentless case is where the false positive lives.
968
+ def class_new_lift(class_name, arg_types, context = nil)
959
969
  return nil unless class_name == "Class"
960
- return Type::Combinator.singleton_of("Object") if arg_types.empty?
970
+
971
+ if arg_types.empty?
972
+ anonymous = anonymous_class_new_name(context)
973
+ return Type::Combinator.singleton_of(anonymous || "Object")
974
+ end
961
975
  return nil unless arg_types.size == 1
962
976
 
963
977
  parent = arg_types.first
@@ -966,6 +980,16 @@ module Rigor
966
980
  nil
967
981
  end
968
982
 
983
+ # The synthetic name `ScopeIndexer` keyed this call site's block body by, or nil when the context carries no
984
+ # call node (internal dispatcher callers) or the call has no block. The source path is passed through
985
+ # verbatim — including when it is nil, which is exactly what `ScopeIndexer` saw for the same file — so the
986
+ # two passes derive the same name whether or not the caller supplied a path.
987
+ def anonymous_class_new_name(context)
988
+ return nil if context.nil?
989
+
990
+ AnonymousMetaClass.name_for(context.call_node, context.scope&.source_path)
991
+ end
992
+
969
993
  # ADR-15 Phase 4b.x — `Ractor.make_shareable` on both the outer Hash and each lambda value.
970
994
  # A plain `.freeze` leaves the Procs unshareable; reading `CONSTANT_CONSTRUCTORS[class]`
971
995
  # from a worker Ractor would raise `Ractor::IsolationError`, which the `rescue
@@ -1000,17 +1024,24 @@ module Rigor
1000
1024
  # `Tuple[…]` when `n` is a small `Constant<Integer>`. Cap at `ARRAY_NEW_TUPLE_LIMIT` (16)
1001
1025
  # so a `Array.new(1_000_000)` does not balloon the carrier; oversize calls fall back to
1002
1026
  # `Nominal[Array]`.
1027
+ #
1028
+ # #317 — `Array.new(n) { |i| ... }` fills every slot from the BLOCK's return type instead
1029
+ # of the two-arg `(n, default_value)` overload's `nil` fill. The block and the trailing
1030
+ # `default_value` positional are mutually exclusive per `Array#initialize`'s RBS overload
1031
+ # set (`(int size, ?E default_value) -> void` vs `(int size) { (Integer index) -> E } ->
1032
+ # void`), so a block present at the call site always wins over any (illegal, but tolerated
1033
+ # rather than rejected here) second positional.
1003
1034
  ARRAY_NEW_TUPLE_LIMIT = 16
1004
1035
  private_constant :ARRAY_NEW_TUPLE_LIMIT
1005
1036
 
1006
- def array_new_lift(class_name, arg_types)
1037
+ def array_new_lift(class_name, arg_types, block_type = nil)
1007
1038
  return nil unless class_name == "Array"
1008
1039
  return nil if arg_types.empty? || arg_types.size > 2
1009
1040
 
1010
1041
  size = array_new_size(arg_types.first)
1011
1042
  return nil if size.nil? || size.negative? || size > ARRAY_NEW_TUPLE_LIMIT
1012
1043
 
1013
- fill = array_new_fill(arg_types[1])
1044
+ fill = block_type || array_new_fill(arg_types[1])
1014
1045
  Type::Combinator.tuple_of(*Array.new(size, fill))
1015
1046
  end
1016
1047