rigortype 0.1.19 → 0.2.1
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 +41 -6
- data/data/core_overlay/numeric.rbs +33 -0
- data/data/core_overlay/pathname.rbs +25 -0
- data/data/core_overlay/string_scanner.rbs +28 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
- data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
- data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
- data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
- data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
- data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
- data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
- data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
- data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
- data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
- data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
- data/data/vendored_gem_sigs/redis/future.rbs +5 -0
- data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
- data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +138 -16
- data/lib/rigor/cli/coverage_command.rb +138 -31
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
- data/lib/rigor/cli/fused_protection_report.rb +76 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +152 -0
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +57 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +76 -5
- data/lib/rigor/environment.rb +66 -8
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +20 -28
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +169 -24
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +271 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +180 -0
- data/lib/rigor/protection/mutator.rb +267 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +14 -2
- data/lib/rigor/scope.rb +54 -11
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/signature_path_audit.rb +92 -0
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +49 -1
|
@@ -45,8 +45,9 @@ module Rigor
|
|
|
45
45
|
# machinery works without duplication: `Tuple[Integer, String]`
|
|
46
46
|
# dispatches as `Array[Integer | String]`, and
|
|
47
47
|
# `HashShape{a: Integer}` dispatches as `Hash[Symbol, Integer]`.
|
|
48
|
-
# Tuple
|
|
49
|
-
# member)
|
|
48
|
+
# Tuple/HashShape element precision (e.g., `tuple[0]` returning
|
|
49
|
+
# the precise member) is handled by the preceding `ShapeDispatch`
|
|
50
|
+
# tier.
|
|
50
51
|
#
|
|
51
52
|
# Remaining limitations:
|
|
52
53
|
#
|
|
@@ -107,17 +108,11 @@ module Rigor
|
|
|
107
108
|
# @return [Rigor::Type, nil] inferred return type, or `nil`
|
|
108
109
|
# when no rule resolves (no class name, no method, dispatch
|
|
109
110
|
# on a Top/Dynamic[Top] receiver, etc.).
|
|
110
|
-
# @param scope [Rigor::Scope, nil] when supplied, enables
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
# that ancestor's RBS. `nil` (the default for every caller
|
|
116
|
-
# that does not thread a scope) keeps the legacy behaviour —
|
|
117
|
-
# such an inherited call stays unresolved and degrades to
|
|
118
|
-
# `Dynamic[Top]`, which is the false-positive-safe default
|
|
119
|
-
# for the open hierarchies (`< ActionController::Base`, …)
|
|
120
|
-
# the allow-list deliberately excludes.
|
|
111
|
+
# @param scope [Rigor::Scope, nil] when supplied, enables ADR-43
|
|
112
|
+
# RBS-complete-ancestor resolution against
|
|
113
|
+
# `ALLOWED_RBS_COMPLETE_ANCESTORS`. `nil` keeps inherited calls
|
|
114
|
+
# unresolved (`Dynamic[Top]`) — the FP-safe default for open
|
|
115
|
+
# hierarchies (`< ActionController::Base`, …).
|
|
121
116
|
def try_dispatch(context)
|
|
122
117
|
environment = context.environment
|
|
123
118
|
return nil if environment.nil?
|
|
@@ -249,15 +244,8 @@ module Rigor
|
|
|
249
244
|
["Array", :instance, tuple_type_args(receiver)]
|
|
250
245
|
when Type::HashShape
|
|
251
246
|
["Hash", :instance, hash_shape_type_args(receiver)]
|
|
252
|
-
when Type::DataInstance
|
|
253
|
-
|
|
254
|
-
# class (or the `Data` supertype) so non-member calls
|
|
255
|
-
# (`inspect`, `==`, `frozen?`, ...) resolve through RBS
|
|
256
|
-
# rather than mis-firing undefined-method. Member reads were
|
|
257
|
-
# already folded by DataFolding above this tier.
|
|
258
|
-
[receiver.class_name || "Data", :instance, []]
|
|
259
|
-
when Type::DataClass
|
|
260
|
-
[receiver.class_name || "Data", :singleton, []]
|
|
247
|
+
when Type::DataInstance, Type::DataClass, Type::StructInstance, Type::StructClass
|
|
248
|
+
member_carrier_descriptor(receiver)
|
|
261
249
|
when Type::BoundMethod
|
|
262
250
|
# `BoundMethod` is a precision-bearing alias for
|
|
263
251
|
# `Nominal[Method]`: it carries the
|
|
@@ -275,6 +263,20 @@ module Rigor
|
|
|
275
263
|
end
|
|
276
264
|
end
|
|
277
265
|
|
|
266
|
+
# ADR-48 — project a `Data`/`Struct` member carrier to its tagging
|
|
267
|
+
# class (or the `Data`/`Struct` supertype) so non-member calls
|
|
268
|
+
# (`inspect`, `==`, `frozen?`, ...) resolve through RBS rather than
|
|
269
|
+
# mis-firing undefined-method. Precise member reads were already
|
|
270
|
+
# folded by DataFolding / StructFolding above this tier.
|
|
271
|
+
def member_carrier_descriptor(receiver)
|
|
272
|
+
case receiver
|
|
273
|
+
when Type::DataInstance then [receiver.class_name || "Data", :instance, []]
|
|
274
|
+
when Type::DataClass then [receiver.class_name || "Data", :singleton, []]
|
|
275
|
+
when Type::StructInstance then [receiver.class_name || "Struct", :instance, []]
|
|
276
|
+
when Type::StructClass then [receiver.class_name || "Struct", :singleton, []]
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
278
280
|
def tuple_type_args(tuple)
|
|
279
281
|
return [] if tuple.elements.empty?
|
|
280
282
|
|
|
@@ -81,6 +81,7 @@ module Rigor
|
|
|
81
81
|
sum: :tuple_sum,
|
|
82
82
|
min: :tuple_min,
|
|
83
83
|
max: :tuple_max,
|
|
84
|
+
minmax: :tuple_minmax_pair,
|
|
84
85
|
sort: :tuple_sort,
|
|
85
86
|
reverse: :tuple_reverse,
|
|
86
87
|
to_a: :tuple_to_a,
|
|
@@ -99,9 +100,15 @@ module Rigor
|
|
|
99
100
|
index: :tuple_find_index,
|
|
100
101
|
find_index: :tuple_find_index,
|
|
101
102
|
rindex: :tuple_rindex,
|
|
102
|
-
flatten: :tuple_flatten
|
|
103
|
+
flatten: :tuple_flatten,
|
|
104
|
+
join: :tuple_join
|
|
103
105
|
}.freeze
|
|
104
106
|
|
|
107
|
+
# Byte cap on a folded `tuple.join` result — a huge tuple times a
|
|
108
|
+
# long separator must not materialise an unbounded `Constant`.
|
|
109
|
+
TUPLE_JOIN_BYTE_LIMIT = 4096
|
|
110
|
+
private_constant :TUPLE_JOIN_BYTE_LIMIT
|
|
111
|
+
|
|
105
112
|
HASH_SHAPE_HANDLERS = {
|
|
106
113
|
size: :hash_size,
|
|
107
114
|
length: :hash_size,
|
|
@@ -573,8 +580,17 @@ module Rigor
|
|
|
573
580
|
%i[lowercase upcase] => :uppercase_string,
|
|
574
581
|
%i[uppercase upcase] => :refined_self,
|
|
575
582
|
%i[uppercase downcase] => :lowercase_string,
|
|
583
|
+
# `numeric-string` is the full Ruby numeric-literal
|
|
584
|
+
# grammar (since the predicate delegates to the
|
|
585
|
+
# parser). `#downcase` preserves it — lowercasing a
|
|
586
|
+
# literal (hex digits, `0X` / `E` prefixes) yields a
|
|
587
|
+
# valid lowercase literal — but `#upcase` does NOT:
|
|
588
|
+
# the rational / imaginary suffixes are lowercase-only
|
|
589
|
+
# (`"1r".upcase == "1R"` is not a literal), so `upcase`
|
|
590
|
+
# drops to the plain base `String` — still sound (the
|
|
591
|
+
# result is a String), just no longer numeric.
|
|
576
592
|
%i[numeric downcase] => :refined_self,
|
|
577
|
-
%i[numeric upcase] => :
|
|
593
|
+
%i[numeric upcase] => :base_string,
|
|
578
594
|
# Digit-only strings are case-invariant; the prefix
|
|
579
595
|
# letters in `0o…` / `0x…` are accepted by the
|
|
580
596
|
# predicate in either case so the predicate-subset
|
|
@@ -587,19 +603,19 @@ module Rigor
|
|
|
587
603
|
%i[hex_int downcase] => :refined_self,
|
|
588
604
|
%i[hex_int upcase] => :refined_self,
|
|
589
605
|
# v0.1.1 Track 1 slice 2 — `to_i` / `to_int` on a
|
|
590
|
-
#
|
|
591
|
-
#
|
|
592
|
-
#
|
|
593
|
-
#
|
|
594
|
-
#
|
|
595
|
-
#
|
|
596
|
-
#
|
|
597
|
-
#
|
|
598
|
-
#
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
%i[
|
|
602
|
-
%i[
|
|
606
|
+
# `decimal-int-string` parses to an `Integer`. The
|
|
607
|
+
# carrier is `universal_int`, NOT `non-negative-int`:
|
|
608
|
+
# the predicate `/\A-?\d+\z/` admits a leading sign, so
|
|
609
|
+
# `"-7"` is a valid decimal-int-string and
|
|
610
|
+
# `"-7".to_i == -7 < 0`. `String#to_i` is total (never
|
|
611
|
+
# raises), so the projection is sound — just signed.
|
|
612
|
+
# `numeric-string` is deliberately NOT projected to
|
|
613
|
+
# `to_i` at all: it now spans the full numeric-literal
|
|
614
|
+
# grammar, so a `"1.5"` / `"2i"` inhabitant has a
|
|
615
|
+
# fractional or non-Integer parse — it falls through to
|
|
616
|
+
# the RBS `Integer`.
|
|
617
|
+
%i[decimal_int to_i] => :universal_int,
|
|
618
|
+
%i[decimal_int to_int] => :universal_int
|
|
603
619
|
})
|
|
604
620
|
private_constant :REFINED_STRING_PROJECTIONS
|
|
605
621
|
|
|
@@ -624,6 +640,8 @@ module Rigor
|
|
|
624
640
|
when :uppercase_string then Type::Combinator.uppercase_string
|
|
625
641
|
when :lowercase_string then Type::Combinator.lowercase_string
|
|
626
642
|
when :non_negative_int then Type::Combinator.non_negative_int
|
|
643
|
+
when :universal_int then Type::Combinator.universal_int
|
|
644
|
+
when :base_string then refined.base
|
|
627
645
|
end
|
|
628
646
|
end
|
|
629
647
|
|
|
@@ -791,6 +809,37 @@ module Rigor
|
|
|
791
809
|
Type::Combinator.constant_of(values.sum)
|
|
792
810
|
end
|
|
793
811
|
|
|
812
|
+
# `tuple.join(sep = "")` — fold to the joined `Constant[String]`
|
|
813
|
+
# when every element is a `Constant` (its `to_s` is deterministic
|
|
814
|
+
# for the scalar value classes) and the separator is absent or a
|
|
815
|
+
# `Constant[String]`. Capped at `TUPLE_JOIN_BYTE_LIMIT`.
|
|
816
|
+
def tuple_join(tuple, _method_name, args)
|
|
817
|
+
sep = tuple_join_separator(args)
|
|
818
|
+
return nil if sep.nil?
|
|
819
|
+
|
|
820
|
+
values = constant_values(tuple.elements)
|
|
821
|
+
return nil if values.nil?
|
|
822
|
+
|
|
823
|
+
result = values.join(sep)
|
|
824
|
+
return nil if result.bytesize > TUPLE_JOIN_BYTE_LIMIT
|
|
825
|
+
|
|
826
|
+
Type::Combinator.constant_of(result)
|
|
827
|
+
rescue StandardError
|
|
828
|
+
nil
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
# The join separator: `""` for the no-arg form, the value of a
|
|
832
|
+
# single `Constant[String]` arg, or `nil` to decline.
|
|
833
|
+
def tuple_join_separator(args)
|
|
834
|
+
return "" if args.empty?
|
|
835
|
+
return nil unless args.size == 1
|
|
836
|
+
|
|
837
|
+
arg = args.first
|
|
838
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
|
|
839
|
+
|
|
840
|
+
arg.value
|
|
841
|
+
end
|
|
842
|
+
|
|
794
843
|
# `tuple.min` / `tuple.max` — fold when every element is
|
|
795
844
|
# a `Constant` whose values share a Ruby-comparable
|
|
796
845
|
# domain. Empty tuples fold to `Constant[nil]`.
|
|
@@ -815,6 +864,32 @@ module Rigor
|
|
|
815
864
|
nil
|
|
816
865
|
end
|
|
817
866
|
|
|
867
|
+
# `tuple.minmax` — the `[min, max]` pair as a 2-slot
|
|
868
|
+
# `Tuple[Constant[min], Constant[max]]`, mirroring the
|
|
869
|
+
# `Range#minmax` fold. Every element must be a `Constant`
|
|
870
|
+
# and the values must Ruby-compare; an empty tuple folds to
|
|
871
|
+
# `Tuple[nil, nil]` (Ruby's `[].minmax`), incomparable
|
|
872
|
+
# mixed-class values decline.
|
|
873
|
+
def tuple_minmax_pair(tuple, _method_name, args)
|
|
874
|
+
return nil unless args.empty?
|
|
875
|
+
|
|
876
|
+
if tuple.elements.empty?
|
|
877
|
+
nil_const = Type::Combinator.constant_of(nil)
|
|
878
|
+
return Type::Combinator.tuple_of(nil_const, nil_const)
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
values = constant_values(tuple.elements)
|
|
882
|
+
return nil if values.nil?
|
|
883
|
+
|
|
884
|
+
low, high = values.minmax
|
|
885
|
+
Type::Combinator.tuple_of(
|
|
886
|
+
Type::Combinator.constant_of(low),
|
|
887
|
+
Type::Combinator.constant_of(high)
|
|
888
|
+
)
|
|
889
|
+
rescue StandardError
|
|
890
|
+
nil
|
|
891
|
+
end
|
|
892
|
+
|
|
818
893
|
# `tuple.sort` — every element must be a `Constant` and
|
|
819
894
|
# the values must Ruby-compare. The result is a Tuple
|
|
820
895
|
# with the same elements in sorted order. Comparison
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
require_relative "singleton_folding"
|
|
5
|
+
require_relative "member_shape_projection"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
module MethodDispatcher
|
|
10
|
+
# ADR-48 Struct follow-up — `Struct.new` value folding, the mutable
|
|
11
|
+
# sibling of {DataFolding}. Same fully-decidable-shape, degrade-on-any-
|
|
12
|
+
# uncertainty discipline, with one extra premise the immutable `Data`
|
|
13
|
+
# path does not need: **mutation soundness.**
|
|
14
|
+
#
|
|
15
|
+
# A `Struct` instance is mutable (`s.x = v`, `s[:x] = v`, escape), so a
|
|
16
|
+
# member map bound to a variable can be invalidated by a later write.
|
|
17
|
+
# This slice (ADR-48 slices 1+2 in the sound *transient* form) folds a
|
|
18
|
+
# member read ONLY off a **fresh** instance — the transient receiver of
|
|
19
|
+
# a `.new(...).x` / `.with(...).x` chain, which provably cannot have
|
|
20
|
+
# been mutated between materialisation and the read. A member read off
|
|
21
|
+
# a *stored* binding degrades to `Dynamic[top]` rather than fold a
|
|
22
|
+
# possibly-stale value. Promoting the fold to mutation-free bound
|
|
23
|
+
# locals is the deferred slice 3 (relax the fresh-receiver gate to a
|
|
24
|
+
# fold-safe-local scan); precise mutated-member re-typing is slice 4.
|
|
25
|
+
#
|
|
26
|
+
# Responsibilities:
|
|
27
|
+
#
|
|
28
|
+
# 1. `Struct.new(:x, :y [, keyword_init: <bool>])` on a
|
|
29
|
+
# `Singleton[Struct]` receiver, literal-Symbol members, NO block ->
|
|
30
|
+
# `StructClass{members:, keyword_init:}`. A block / non-literal
|
|
31
|
+
# members defer.
|
|
32
|
+
# 2. `.new` / `.[]` on a `StructClass` (or a `Singleton[Point]` with a
|
|
33
|
+
# recorded struct layout) -> a `StructInstance`, the member map built
|
|
34
|
+
# from the call's arguments (positional or keyword per the class's
|
|
35
|
+
# `keyword_init`). A form / arity mismatch degrades to `Dynamic[top]`
|
|
36
|
+
# rather than a wrong member map. `.members` on the class folds.
|
|
37
|
+
# 3. member reads + `[]` / `to_h` / `deconstruct` / `deconstruct_keys`
|
|
38
|
+
# / `members` / `with` on a *fresh* `StructInstance` -> the precise
|
|
39
|
+
# projected type; member *setters* (`s.x = v`) return the assigned
|
|
40
|
+
# value type. Unhandled / stored-receiver calls return nil so the
|
|
41
|
+
# pipeline projects the instance to its nominal through RbsDispatch.
|
|
42
|
+
#
|
|
43
|
+
# See docs/adr/48-data-struct-value-folding.md § "Struct follow-up".
|
|
44
|
+
module StructFolding
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
# The `[]` / `to_h` / `deconstruct` / `members` / `with` projections
|
|
48
|
+
# and the reader-redefinition guard are shared with {DataFolding}.
|
|
49
|
+
extend MemberShapeProjection
|
|
50
|
+
|
|
51
|
+
# @return [Rigor::Type, nil] the folded result, or nil to defer.
|
|
52
|
+
def try_dispatch(context)
|
|
53
|
+
receiver = context.receiver
|
|
54
|
+
|
|
55
|
+
return fold_struct_new(context) if SingletonFolding.receiver?(receiver, "Struct")
|
|
56
|
+
|
|
57
|
+
case receiver
|
|
58
|
+
when Type::StructClass
|
|
59
|
+
dispatch_struct_class(receiver, context)
|
|
60
|
+
when Type::StructInstance
|
|
61
|
+
fold_instance(receiver, context)
|
|
62
|
+
when Type::Singleton
|
|
63
|
+
fold_named_new(receiver, context)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A `Struct.new`-defined class assigned to a constant (or a
|
|
68
|
+
# `class Point < Struct.new(...)` subclass) is canonicalised by the
|
|
69
|
+
# engine to `Singleton[Point]`, not a `StructClass` — so its member
|
|
70
|
+
# layout is read from the project side-table the scope indexer built
|
|
71
|
+
# (`Scope#struct_member_layout`) rather than from the receiver
|
|
72
|
+
# carrier.
|
|
73
|
+
def fold_named_new(singleton, context)
|
|
74
|
+
scope = context.scope
|
|
75
|
+
return nil if scope.nil?
|
|
76
|
+
|
|
77
|
+
layout = scope.struct_member_layout(singleton.class_name)
|
|
78
|
+
return nil if layout.nil?
|
|
79
|
+
|
|
80
|
+
materialize_instance(layout[:members], layout[:keyword_init], singleton.class_name, context)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# --- 1. Struct.new(:x, :y) --------------------------------------
|
|
84
|
+
|
|
85
|
+
def fold_struct_new(context)
|
|
86
|
+
return nil unless context.method_name == :new
|
|
87
|
+
# Block-form (`Struct.new(:x) do ... end`) defers — the named
|
|
88
|
+
# constant/subclass forms still fold via the layout side-table.
|
|
89
|
+
return nil unless context.block_type.nil?
|
|
90
|
+
|
|
91
|
+
parsed = parse_struct_new_args(context.args)
|
|
92
|
+
return nil if parsed.nil?
|
|
93
|
+
|
|
94
|
+
Type::Combinator.struct_class_of(members: parsed[:members], keyword_init: parsed[:keyword_init])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Parses `Struct.new`'s arguments into `{ members:, keyword_init: }`,
|
|
98
|
+
# or nil when any argument is non-conforming (a dynamic member name,
|
|
99
|
+
# an unexpected trailing keyword). Handles the optional leading String
|
|
100
|
+
# class name and the trailing `keyword_init:` option hash.
|
|
101
|
+
def parse_struct_new_args(args)
|
|
102
|
+
rest = args.dup
|
|
103
|
+
keyword_init = false
|
|
104
|
+
|
|
105
|
+
if rest.last.is_a?(Type::HashShape)
|
|
106
|
+
options = rest.pop
|
|
107
|
+
return nil unless struct_options_hash?(options)
|
|
108
|
+
|
|
109
|
+
value = options.pairs[:keyword_init]
|
|
110
|
+
keyword_init = value.is_a?(Type::Constant) && value.value == true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Optional leading String name: `Struct.new("Point", :x, :y)`.
|
|
114
|
+
rest = rest[1..] if rest.first.is_a?(Type::Constant) && rest.first.value.is_a?(String)
|
|
115
|
+
|
|
116
|
+
members = []
|
|
117
|
+
rest.each do |arg|
|
|
118
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Symbol)
|
|
119
|
+
|
|
120
|
+
members << arg.value
|
|
121
|
+
end
|
|
122
|
+
return nil if members.empty?
|
|
123
|
+
return nil unless members.uniq.size == members.size
|
|
124
|
+
|
|
125
|
+
{ members: members, keyword_init: keyword_init }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# A trailing hash is the `Struct.new` options hash only when it is a
|
|
129
|
+
# closed shape whose sole key is `:keyword_init`. Any other key means
|
|
130
|
+
# the call is not a recognisable member-list definition -> defer.
|
|
131
|
+
def struct_options_hash?(shape)
|
|
132
|
+
shape.closed? && shape.optional_keys.empty? &&
|
|
133
|
+
shape.pairs.keys.all? { |key| key == :keyword_init }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# --- 2. Point.new(...) / Point[...] / Point.members -------------
|
|
137
|
+
|
|
138
|
+
def dispatch_struct_class(struct_class, context)
|
|
139
|
+
case context.method_name
|
|
140
|
+
when :new, :[]
|
|
141
|
+
materialize_instance(struct_class.members, struct_class.keyword_init,
|
|
142
|
+
struct_class.class_name, context)
|
|
143
|
+
when :members
|
|
144
|
+
Type::Combinator.tuple_of(*struct_class.members.map { |name| Type::Combinator.constant_of(name) })
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def materialize_instance(members, keyword_init, class_name, context)
|
|
149
|
+
return nil unless %i[new []].include?(context.method_name)
|
|
150
|
+
|
|
151
|
+
map = member_map_for_new(members, keyword_init, context)
|
|
152
|
+
return degraded_instance if map.nil?
|
|
153
|
+
|
|
154
|
+
Type::Combinator.struct_instance_of(members: map, class_name: class_name)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Builds the member -> type map honouring the class's `keyword_init`
|
|
158
|
+
# flag: a `keyword_init: true` struct accepts only the keyword form,
|
|
159
|
+
# a positional struct only the positional form. The mismatched form
|
|
160
|
+
# is a different runtime shape, so it degrades rather than fold.
|
|
161
|
+
def member_map_for_new(members, keyword_init, context)
|
|
162
|
+
if keyword_new?(context)
|
|
163
|
+
keyword_init ? keyword_member_map(members, context.args) : nil
|
|
164
|
+
else
|
|
165
|
+
keyword_init ? nil : positional_member_map(members, context.args)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# `Point.new(x: 1)` arrives as a single trailing `HashShape` whose
|
|
170
|
+
# call node is a `KeywordHashNode`; distinguishing it from a
|
|
171
|
+
# positional hash needs the call node (both type to a `HashShape`).
|
|
172
|
+
def keyword_new?(context)
|
|
173
|
+
node = context.call_node
|
|
174
|
+
return false if node.nil?
|
|
175
|
+
|
|
176
|
+
arguments = node.arguments&.arguments
|
|
177
|
+
return false if arguments.nil? || arguments.empty?
|
|
178
|
+
|
|
179
|
+
arguments.last.is_a?(Prism::KeywordHashNode)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# `Struct.new(:a, :b).new(v)` is legal — trailing members default to
|
|
183
|
+
# `nil` — so fewer positionals than members pad with `Constant[nil]`;
|
|
184
|
+
# more positionals than members is an ArgumentError -> degrade.
|
|
185
|
+
def positional_member_map(members, args)
|
|
186
|
+
return nil if args.size > members.size
|
|
187
|
+
|
|
188
|
+
members.each_with_index.to_h do |name, index|
|
|
189
|
+
[name, index < args.size ? args[index] : Type::Combinator.constant_of(nil)]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# `Struct.new(:a, :b).new(a: 1)` is legal — omitted members default
|
|
194
|
+
# to `nil` — so a keyword subset pads the rest; an unknown key is an
|
|
195
|
+
# ArgumentError -> degrade.
|
|
196
|
+
def keyword_member_map(members, args)
|
|
197
|
+
return nil unless args.size == 1
|
|
198
|
+
|
|
199
|
+
shape = args.first
|
|
200
|
+
return nil unless shape.is_a?(Type::HashShape) && shape.closed?
|
|
201
|
+
return nil unless shape.optional_keys.empty?
|
|
202
|
+
return nil unless shape.pairs.keys.all? { |key| members.include?(key) }
|
|
203
|
+
|
|
204
|
+
members.to_h { |name| [name, shape.pairs[name] || Type::Combinator.constant_of(nil)] }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# A `.new` whose arguments cannot soundly populate the member map
|
|
208
|
+
# degrades to `Dynamic[top]` (today's behaviour for `Struct.new(...)`
|
|
209
|
+
# instances), never a wrong map.
|
|
210
|
+
def degraded_instance
|
|
211
|
+
Type::Combinator.untyped
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# --- 3. inst.x / inst.x = v / inst[...] / inst.to_h / ... -------
|
|
215
|
+
|
|
216
|
+
def fold_instance(instance, context)
|
|
217
|
+
method_name = context.method_name
|
|
218
|
+
args = context.args
|
|
219
|
+
members = instance.members
|
|
220
|
+
|
|
221
|
+
# Member setter `s.x = v`: returns the assigned value type. Sound
|
|
222
|
+
# regardless of mutation state (it models the setter's own return),
|
|
223
|
+
# and avoids a fall-through undefined-method on a writer the
|
|
224
|
+
# existence table does not register.
|
|
225
|
+
setter = member_setter_target(method_name, members)
|
|
226
|
+
return args.first if setter && args.size == 1
|
|
227
|
+
|
|
228
|
+
foldable = foldable_receiver?(context)
|
|
229
|
+
|
|
230
|
+
if members.key?(method_name) && args.empty? && !reader_overridden?(instance, method_name, context.scope)
|
|
231
|
+
# A stored receiver folds only when the bound local is proven
|
|
232
|
+
# fold-safe (ADR-48 slice 3 — never mutated / aliased / escaped);
|
|
233
|
+
# otherwise the member value may be stale, so it degrades to
|
|
234
|
+
# `Dynamic[top]` (not nil -> no undefined-method fall-through).
|
|
235
|
+
return foldable ? members.fetch(method_name) : Type::Combinator.untyped
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Projections fold only off a foldable (fresh, or proven fold-safe)
|
|
239
|
+
# instance; off any other stored binding they defer to Struct's RBS
|
|
240
|
+
# (`to_h` / `[]` / `members` / `deconstruct*` all exist on `Struct`),
|
|
241
|
+
# which is sound and non-regressive.
|
|
242
|
+
return nil unless foldable
|
|
243
|
+
|
|
244
|
+
fold_fresh_projection(instance, method_name, args)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def fold_fresh_projection(instance, method_name, args)
|
|
248
|
+
case method_name
|
|
249
|
+
when :[] then instance_index(instance, args)
|
|
250
|
+
when :to_h, :to_hash then instance_to_h(instance)
|
|
251
|
+
when :deconstruct then instance_deconstruct(instance)
|
|
252
|
+
when :deconstruct_keys then instance_deconstruct_keys(instance, args)
|
|
253
|
+
when :members then instance_members(instance)
|
|
254
|
+
when :with
|
|
255
|
+
instance_with(instance, args) do |members, class_name|
|
|
256
|
+
Type::Combinator.struct_instance_of(members: members, class_name: class_name)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# The member name a `:<member>=` setter targets, or nil. Comparison
|
|
262
|
+
# operators (`==`, `>=`, ...) end with `=` too but never strip to a
|
|
263
|
+
# member symbol, so they fall through to the normal dispatch path.
|
|
264
|
+
def member_setter_target(method_name, members)
|
|
265
|
+
name = method_name.to_s
|
|
266
|
+
return nil unless name.end_with?("=")
|
|
267
|
+
|
|
268
|
+
base = name[0..-2].to_sym
|
|
269
|
+
members.key?(base) ? base : nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# A member read is foldable when the receiver is either FRESH (the
|
|
273
|
+
# transient result of a `.new(...)`/`.with(...)` chain, which cannot
|
|
274
|
+
# have been mutated between materialisation and this read) or a
|
|
275
|
+
# FOLD-SAFE stored local (ADR-48 slice 3 — `StructFoldSafety` proved
|
|
276
|
+
# the binding is never mutated / aliased / escaped in its scope).
|
|
277
|
+
def foldable_receiver?(context)
|
|
278
|
+
fresh_receiver?(context) || fold_safe_local_receiver?(context)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# A fresh receiver is the transient result of a chained call
|
|
282
|
+
# (`Point.new(1, 2).x`, `inst.with(x: 9).y`).
|
|
283
|
+
def fresh_receiver?(context)
|
|
284
|
+
node = context.call_node
|
|
285
|
+
return false if node.nil?
|
|
286
|
+
|
|
287
|
+
node.receiver.is_a?(Prism::CallNode)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# A fold-safe stored receiver is a local-variable read whose name the
|
|
291
|
+
# body's fold-safe set (on the scope) marks as never mutated.
|
|
292
|
+
def fold_safe_local_receiver?(context)
|
|
293
|
+
node = context.call_node
|
|
294
|
+
receiver = node&.receiver
|
|
295
|
+
scope = context.scope
|
|
296
|
+
return false unless receiver.is_a?(Prism::LocalVariableReadNode) && scope
|
|
297
|
+
|
|
298
|
+
scope.struct_fold_safe?(receiver.name)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|