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.
- checksums.yaml +4 -4
- data/README.md +10 -10
- data/data/core_overlay/pathname.rbs +5 -0
- data/data/core_overlay/resolv.rbs +31 -0
- data/data/core_overlay/string_scanner.rbs +5 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
- data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
- data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
- data/docs/manual/02-cli-reference.md +71 -11
- data/docs/manual/03-configuration.md +16 -1
- data/docs/manual/04-diagnostics.md +8 -0
- data/docs/manual/12-caching.md +17 -5
- data/docs/manual/15-type-protection-coverage.md +122 -1
- data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +107 -28
- data/lib/rigor/analysis/incremental_session.rb +156 -11
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/run_cache_key.rb +17 -1
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
- data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
- data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
- data/lib/rigor/analysis/runner.rb +78 -7
- data/lib/rigor/analysis/severity_stamp.rb +2 -1
- data/lib/rigor/bleeding_edge.rb +132 -7
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +162 -0
- data/lib/rigor/cache/incremental_snapshot.rb +87 -18
- data/lib/rigor/cli/check_command.rb +49 -28
- data/lib/rigor/cli/coverage_command.rb +15 -2
- data/lib/rigor/cli/coverage_mutation.rb +242 -11
- data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
- data/lib/rigor/cli/fused_protection_report.rb +12 -3
- data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
- data/lib/rigor/cli/mutation_protection_report.rb +26 -5
- data/lib/rigor/cli/protection_renderer.rb +13 -0
- data/lib/rigor/cli/protection_report.rb +11 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
- data/lib/rigor/cli/sig_gen_command.rb +5 -4
- data/lib/rigor/configuration.rb +45 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +434 -75
- data/lib/rigor/environment.rb +28 -13
- data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +62 -4
- data/lib/rigor/inference/fork_map.rb +47 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +158 -12
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +234 -11
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
- data/lib/rigor/inference/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/mutation_widening.rb +30 -25
- data/lib/rigor/inference/narrowing.rb +3 -1
- data/lib/rigor/inference/optimistic_origin.rb +123 -0
- data/lib/rigor/inference/protection_scanner.rb +18 -2
- data/lib/rigor/inference/receiver_alias.rb +57 -0
- data/lib/rigor/inference/scope_indexer.rb +197 -55
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +55 -4
- data/lib/rigor/language_server/buffer_table.rb +22 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +203 -4
- data/lib/rigor/language_server/project_context.rb +45 -0
- data/lib/rigor/language_server/publish_batcher.rb +82 -0
- data/lib/rigor/language_server/server.rb +20 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/node_rule_walk.rb +6 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/protection/closure_kill_oracle.rb +190 -0
- data/lib/rigor/protection/dependency_closure.rb +59 -0
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
- data/lib/rigor/protection/discovery_seed.rb +116 -0
- data/lib/rigor/protection/kill_signature.rb +31 -0
- data/lib/rigor/protection/mutation_cache.rb +355 -0
- data/lib/rigor/protection/mutation_scanner.rb +55 -11
- data/lib/rigor/protection/mutator.rb +26 -4
- data/lib/rigor/runtime/jit.rb +63 -1
- data/lib/rigor/scope/discovery_index.rb +9 -0
- data/lib/rigor/scope.rb +114 -10
- data/lib/rigor/sig_gen/generator.rb +130 -31
- data/lib/rigor/sig_gen/layout_index.rb +6 -0
- data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
- data/lib/rigor/sig_gen/renderer.rb +20 -1
- data/lib/rigor/sig_gen/write_result.rb +6 -4
- data/lib/rigor/sig_gen/writer.rb +12 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/node_walker.rb +12 -0
- data/lib/rigor/type/anonymous_class_name.rb +40 -0
- data/lib/rigor/type/nominal.rb +5 -2
- data/lib/rigor/type/singleton.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
- data/sig/rigor/inference/void_origin.rbs +6 -3
- data/sig/rigor/scope.rbs +12 -0
- data/sig/rigor.rbs +4 -0
- 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
|
|
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
|
|
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).
|
|
676
|
-
#
|
|
677
|
-
#
|
|
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
|
|
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
|
|
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.
|
|
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);
|
|
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),
|
|
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
|
-
# -
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|