rigortype 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/docs/handbook/11-sig-gen.md +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +136 -7
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +1088 -213
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +59 -7
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
|
@@ -190,6 +190,44 @@ module Rigor
|
|
|
190
190
|
|
|
191
191
|
STRING_FOLD_BYTE_LIMIT = 4096
|
|
192
192
|
|
|
193
|
+
# #2115 — the numeric twin of `STRING_FOLD_BYTE_LIMIT`. A folded Integer (and each Integer part of a
|
|
194
|
+
# folded Rational / Complex) is at most this many bits, the largest bit length whose decimal
|
|
195
|
+
# rendering still fits in `STRING_FOLD_BYTE_LIMIT` digits: `2 ** 13_605` folds, `2 ** 13_606`
|
|
196
|
+
# (bit length 13_607) does not. Computing `2 ** 100_000_000` takes Ruby milliseconds, but
|
|
197
|
+
# rendering it in decimal — which describing, sorting and printing the `Constant` does — takes
|
|
198
|
+
# minutes. `**` / `pow` / `<<` / `>>` also check their operands before invoking, so the big value
|
|
199
|
+
# is never computed.
|
|
200
|
+
INTEGER_FOLD_BIT_LIMIT = (STRING_FOLD_BYTE_LIMIT * Math.log2(10)).floor
|
|
201
|
+
|
|
202
|
+
# #2115 — the most elements a fold walks out of a `Constant<Range>` receiver. A method that
|
|
203
|
+
# enumerates the range (`to_a`, `to_set`, `all?`, `include?` on a String range, …) declines when
|
|
204
|
+
# the range holds more elements, or is endless (`1..Float::INFINITY`); a method that reads only
|
|
205
|
+
# the endpoints (`RANGE_ENDPOINT_METHODS`) still folds whatever the range's size.
|
|
206
|
+
RANGE_ENUMERATION_LIMIT = 1024
|
|
207
|
+
|
|
208
|
+
# The `Range` methods whose CRuby body reads the endpoints without walking the elements, so they
|
|
209
|
+
# stay foldable on a range past `RANGE_ENUMERATION_LIMIT`. `include?` / `member?` join them on a
|
|
210
|
+
# numeric range, where CRuby answers them by `cover?`.
|
|
211
|
+
RANGE_ENDPOINT_METHODS = Set[
|
|
212
|
+
:begin, :end, :exclude_end?, :cover?, :===, :size, :to_s, :inspect, :==, :eql?, :hash
|
|
213
|
+
].freeze
|
|
214
|
+
RANGE_COVER_METHODS = Set[:include?, :member?].freeze
|
|
215
|
+
private_constant :RANGE_ENDPOINT_METHODS, :RANGE_COVER_METHODS
|
|
216
|
+
|
|
217
|
+
# A result `INTEGER_FOLD_BIT_LIMIT` keeps from becoming a `Constant`: the fold still knows its class,
|
|
218
|
+
# and answers `Nominal[class_name]` for it. Falling through to RBS instead would read `Integer#**`'s
|
|
219
|
+
# declared `Numeric`, on which `(2 ** 20_000).even?` is an undefined method.
|
|
220
|
+
WidenedResult = Data.define(:class_name)
|
|
221
|
+
|
|
222
|
+
# #2125 — a String result past `STRING_FOLD_BYTE_LIMIT`, or an operand that would build one. Unlike a
|
|
223
|
+
# `WidenedResult` it declines the WHOLE fold, every other member of a union receiver included: dropping
|
|
224
|
+
# only the oversized member would answer the rest (`(c ? "a" : big) * 2` read `"aa"`). The next tier then
|
|
225
|
+
# answers the call — `LiteralStringFolding`'s `literal-string`, or RBS's `String`, never less than the
|
|
226
|
+
# `Nominal[String]` the fold could have widened to.
|
|
227
|
+
OversizedString = Data.define
|
|
228
|
+
OVERSIZED_STRING = [OversizedString.new].freeze
|
|
229
|
+
private_constant :OversizedString, :OVERSIZED_STRING
|
|
230
|
+
|
|
193
231
|
# Input cartesian product hard cap. Keeps fold cost bounded even when the receiver and argument are
|
|
194
232
|
# both `Union[Constant…]`. 5 × 5 = 25 inputs is permitted; 6 × 6 = 36 is not. The user-facing payoff
|
|
195
233
|
# (a precise small enum) drops off fast past this range and CRuby method invocation cost adds up.
|
|
@@ -302,6 +340,7 @@ module Rigor
|
|
|
302
340
|
arg = args.first
|
|
303
341
|
ruby_arg = format_argument_value(arg)
|
|
304
342
|
return nil if ruby_arg.nil?
|
|
343
|
+
return nil if format_blow_up?(receiver.value, ruby_arg)
|
|
305
344
|
|
|
306
345
|
result = receiver.value % ruby_arg
|
|
307
346
|
return nil unless foldable_constant_value?(result)
|
|
@@ -649,45 +688,41 @@ module Rigor
|
|
|
649
688
|
def range_constant_unary(range, method_name)
|
|
650
689
|
case method_name
|
|
651
690
|
when :to_a, :entries then range_to_a_tuple(range)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
691
|
+
# #2115 — `Range#min` / `#max` / `#minmax` / `#size` are O(1) on Integer endpoints in CRuby, so
|
|
692
|
+
# none of these materialises the range. The no-arg `first` / `last` read the endpoints, not the
|
|
693
|
+
# elements: `first` is `begin` and `last` is `end` whether the range is empty, reversed or
|
|
694
|
+
# exclusive (`(5..1).first` is 5, `(1...10).last` is 10). Only `min` / `max` depend on emptiness
|
|
695
|
+
# and exclusivity (`(5..1).min` is nil, `(1...10).max` is 9).
|
|
696
|
+
when :first then Type::Combinator.constant_of(range.begin)
|
|
697
|
+
when :last then Type::Combinator.constant_of(range.end)
|
|
698
|
+
when :min then Type::Combinator.constant_of(range.min)
|
|
699
|
+
when :max then Type::Combinator.constant_of(range.max)
|
|
700
|
+
when :count, :size, :length then Type::Combinator.constant_of(range.size)
|
|
701
|
+
when :minmax then Type::Combinator.tuple_of(*range.minmax.map { |v| Type::Combinator.constant_of(v) })
|
|
656
702
|
# `range.sum` is closed-form (Gauss) for an integer range, so a huge range still costs O(1) and
|
|
657
|
-
# yields a single Integer — no materialisation
|
|
658
|
-
# by the Integer-endpoint guard in the caller.
|
|
659
|
-
when :sum then
|
|
703
|
+
# yields a single Integer — no materialisation; only `INTEGER_FOLD_BIT_LIMIT` applies. Endless
|
|
704
|
+
# ranges are already excluded by the Integer-endpoint guard in the caller.
|
|
705
|
+
when :sum then range_sum_constant(range)
|
|
660
706
|
end
|
|
661
707
|
end
|
|
662
708
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
709
|
+
# The Gauss sum of a range of large endpoints can still pass `INTEGER_FOLD_BIT_LIMIT`; it is then
|
|
710
|
+
# an `Integer` (both endpoints are).
|
|
711
|
+
def range_sum_constant(range)
|
|
712
|
+
sum = range.sum
|
|
713
|
+
return Type::Combinator.nominal_of("Integer") if oversized_number?(sum)
|
|
667
714
|
|
|
668
|
-
Type::Combinator.
|
|
715
|
+
Type::Combinator.constant_of(sum)
|
|
669
716
|
end
|
|
670
717
|
|
|
671
|
-
def
|
|
672
|
-
|
|
673
|
-
return
|
|
674
|
-
|
|
675
|
-
Type::Combinator.constant_of(edge == :first ? values.first : values.last)
|
|
676
|
-
end
|
|
718
|
+
def range_to_a_tuple(range)
|
|
719
|
+
# `Range#size` is O(1) on Integer endpoints: check the cardinality before materialising.
|
|
720
|
+
return nil if range.size > RANGE_TO_A_LIMIT
|
|
677
721
|
|
|
678
|
-
def range_minmax_tuple(range)
|
|
679
722
|
values = range.to_a
|
|
680
|
-
if values.empty?
|
|
681
|
-
return Type::Combinator.tuple_of(
|
|
682
|
-
Type::Combinator.constant_of(nil),
|
|
683
|
-
Type::Combinator.constant_of(nil)
|
|
684
|
-
)
|
|
685
|
-
end
|
|
723
|
+
return Type::Combinator.tuple_of if values.empty?
|
|
686
724
|
|
|
687
|
-
Type::Combinator.tuple_of(
|
|
688
|
-
Type::Combinator.constant_of(values.first),
|
|
689
|
-
Type::Combinator.constant_of(values.last)
|
|
690
|
-
)
|
|
725
|
+
Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
|
|
691
726
|
end
|
|
692
727
|
|
|
693
728
|
# `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head / tail forms. `first`/`last` already
|
|
@@ -1175,9 +1210,23 @@ module Rigor
|
|
|
1175
1210
|
# "Integer-ness" was already implied by the inputs (so the widening fallback only fires for
|
|
1176
1211
|
# arithmetic over integers).
|
|
1177
1212
|
def build_constant_type(values, source: nil)
|
|
1213
|
+
return nil if values.any?(OversizedString)
|
|
1214
|
+
|
|
1215
|
+
widened, values = values.partition { |value| value.is_a?(WidenedResult) }
|
|
1216
|
+
return build_scalar_constant_type(values, source) if widened.empty?
|
|
1217
|
+
|
|
1218
|
+
nominals = widened.map(&:class_name).uniq.map { |name| Type::Combinator.nominal_of(name) }
|
|
1219
|
+
return Type::Combinator.union(*nominals) if values.empty?
|
|
1220
|
+
|
|
1221
|
+
constants = build_scalar_constant_type(values, source)
|
|
1222
|
+
constants && Type::Combinator.union(constants, *nominals)
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
def build_scalar_constant_type(values, source)
|
|
1178
1226
|
return nil if values.empty?
|
|
1179
1227
|
|
|
1180
|
-
|
|
1228
|
+
# #1513 — keyed so the possible results `0.0` and `-0.0` stay two members, as `Constant#==` keeps them.
|
|
1229
|
+
unique = values.uniq { |value| Type::Constant.value_key(value) }
|
|
1181
1230
|
return collapse_constants(unique) if unique.size <= UNION_FOLD_OUTPUT_LIMIT
|
|
1182
1231
|
|
|
1183
1232
|
widen_to_integer_range(unique, source) ||
|
|
@@ -1607,9 +1656,13 @@ module Rigor
|
|
|
1607
1656
|
# `flat_map`.
|
|
1608
1657
|
def invoke_binary(receiver_value, method_name, arg_value)
|
|
1609
1658
|
return nil unless safe?(receiver_value, method_name, arg_value)
|
|
1659
|
+
return OVERSIZED_STRING if string_blow_up?(receiver_value, method_name, arg_value)
|
|
1660
|
+
return nil if range_enumeration_blow_up?(receiver_value, method_name)
|
|
1610
1661
|
|
|
1611
|
-
|
|
1612
|
-
|
|
1662
|
+
widened = numeric_blow_up_class(receiver_value, method_name, arg_value)
|
|
1663
|
+
return [WidenedResult.new(widened)] if widened
|
|
1664
|
+
|
|
1665
|
+
fold_result(receiver_value.public_send(method_name, arg_value))
|
|
1613
1666
|
rescue StandardError
|
|
1614
1667
|
nil
|
|
1615
1668
|
end
|
|
@@ -1619,9 +1672,9 @@ module Rigor
|
|
|
1619
1672
|
# `false`/`nil` folds.
|
|
1620
1673
|
def invoke_ternary(receiver_value, method_name, av0, av1)
|
|
1621
1674
|
return nil unless ternary_method_allowed?(receiver_value, method_name)
|
|
1675
|
+
return OVERSIZED_STRING if string_ternary_blow_up?(receiver_value, method_name, av0, av1)
|
|
1622
1676
|
|
|
1623
|
-
|
|
1624
|
-
foldable_constant_value?(result) ? [result] : nil
|
|
1677
|
+
fold_result(receiver_value.public_send(method_name, av0, av1))
|
|
1625
1678
|
rescue StandardError
|
|
1626
1679
|
nil
|
|
1627
1680
|
end
|
|
@@ -1629,14 +1682,23 @@ module Rigor
|
|
|
1629
1682
|
# Returns `[value]` on success, `nil` to signal "skip". See `invoke_binary` for why we wrap.
|
|
1630
1683
|
def invoke_unary(receiver_value, method_name)
|
|
1631
1684
|
return nil unless unary_safe?(receiver_value, method_name)
|
|
1632
|
-
return
|
|
1685
|
+
return OVERSIZED_STRING if string_unary_blow_up?(receiver_value, method_name)
|
|
1686
|
+
return nil if range_enumeration_blow_up?(receiver_value, method_name)
|
|
1633
1687
|
|
|
1634
|
-
|
|
1635
|
-
foldable_constant_value?(result) ? [result] : nil
|
|
1688
|
+
fold_result(receiver_value.public_send(method_name))
|
|
1636
1689
|
rescue StandardError
|
|
1637
1690
|
nil
|
|
1638
1691
|
end
|
|
1639
1692
|
|
|
1693
|
+
# `[result]` for a foldable value, `[WidenedResult]` for a number past `INTEGER_FOLD_BIT_LIMIT`,
|
|
1694
|
+
# `OVERSIZED_STRING` for a String past `STRING_FOLD_BYTE_LIMIT`, nil for anything else.
|
|
1695
|
+
def fold_result(result)
|
|
1696
|
+
return [WidenedResult.new(result.class.name)] if oversized_number?(result)
|
|
1697
|
+
return OVERSIZED_STRING if oversized_string?(result)
|
|
1698
|
+
|
|
1699
|
+
foldable_constant_value?(result) ? [result] : nil
|
|
1700
|
+
end
|
|
1701
|
+
|
|
1640
1702
|
def unary_safe?(receiver_value, method_name)
|
|
1641
1703
|
return true if unary_ops_for(receiver_value).include?(method_name)
|
|
1642
1704
|
|
|
@@ -1775,7 +1837,54 @@ module Rigor
|
|
|
1775
1837
|
# with the widened class.
|
|
1776
1838
|
return false if value.is_a?(Float) && value.nan?
|
|
1777
1839
|
return false if value.is_a?(Complex) && complex_nan?(value)
|
|
1840
|
+
return false if oversized_number?(value)
|
|
1841
|
+
return false if oversized_string?(value)
|
|
1842
|
+
|
|
1843
|
+
true
|
|
1844
|
+
end
|
|
1845
|
+
|
|
1846
|
+
# #2125 — the result gate every String-producing fold passes through, the String twin of
|
|
1847
|
+
# `oversized_number?`. The operand prechecks (`string_blow_up?`, `string_ternary_blow_up?`,
|
|
1848
|
+
# `format_blow_up?`) keep the costly cases from being built at all; this catches the rest after the
|
|
1849
|
+
# fact (`tr` into multibyte characters, `dump`, `Integer#to_s(2)`, whose receiver
|
|
1850
|
+
# `INTEGER_FOLD_BIT_LIMIT` already bounds, so its rendering is at most 13 KB).
|
|
1851
|
+
def oversized_string?(value)
|
|
1852
|
+
value.is_a?(String) && value.bytesize > STRING_FOLD_BYTE_LIMIT
|
|
1853
|
+
end
|
|
1854
|
+
|
|
1855
|
+
# True when an Integer, or an Integer part of a Rational / Complex, is past `INTEGER_FOLD_BIT_LIMIT`.
|
|
1856
|
+
def oversized_number?(value)
|
|
1857
|
+
case value
|
|
1858
|
+
when Integer then value.bit_length > INTEGER_FOLD_BIT_LIMIT
|
|
1859
|
+
when Rational then oversized_number?(value.numerator) || oversized_number?(value.denominator)
|
|
1860
|
+
when Complex then oversized_number?(value.real) || oversized_number?(value.imaginary)
|
|
1861
|
+
else false
|
|
1862
|
+
end
|
|
1863
|
+
end
|
|
1864
|
+
|
|
1865
|
+
# #2115 — true when `receiver_value` is a `Range` the method would walk element by element and the
|
|
1866
|
+
# range holds more than `RANGE_ENUMERATION_LIMIT` elements (an endless one never stops).
|
|
1867
|
+
def range_enumeration_blow_up?(receiver_value, method_name)
|
|
1868
|
+
return false unless receiver_value.is_a?(Range)
|
|
1869
|
+
return false if RANGE_ENDPOINT_METHODS.include?(method_name)
|
|
1870
|
+
return false if RANGE_COVER_METHODS.include?(method_name) && numeric_range?(receiver_value)
|
|
1871
|
+
|
|
1872
|
+
!range_within_enumeration_budget?(receiver_value)
|
|
1873
|
+
end
|
|
1874
|
+
|
|
1875
|
+
def numeric_range?(range)
|
|
1876
|
+
range.begin.is_a?(Numeric) || range.end.is_a?(Numeric)
|
|
1877
|
+
end
|
|
1778
1878
|
|
|
1879
|
+
# Takes at most `RANGE_ENUMERATION_LIMIT + 1` elements (`Range#first(n)` stops there, endless or
|
|
1880
|
+
# not). A range that cannot iterate at all (a Float or Time begin) raises, and so does every
|
|
1881
|
+
# enumerating method on it before doing any work, so it counts as within budget: the invoke's own
|
|
1882
|
+
# rescue declines an enumerating fold, and `(t0..t1).include?(t)`, which CRuby answers by `cover?`,
|
|
1883
|
+
# keeps folding. A beginless range raises `RangeError` here, which the invoke's own rescue turns into
|
|
1884
|
+
# a declined fold, as the enumerating method itself would raise.
|
|
1885
|
+
def range_within_enumeration_budget?(range)
|
|
1886
|
+
range.first(RANGE_ENUMERATION_LIMIT + 1).size <= RANGE_ENUMERATION_LIMIT
|
|
1887
|
+
rescue TypeError
|
|
1779
1888
|
true
|
|
1780
1889
|
end
|
|
1781
1890
|
|
|
@@ -1791,11 +1900,62 @@ module Rigor
|
|
|
1791
1900
|
catalog_allows?(receiver_value, method_name)
|
|
1792
1901
|
return false unless allowed
|
|
1793
1902
|
return false if integer_division_by_zero?(receiver_value, method_name, arg_value)
|
|
1794
|
-
return false if string_blow_up?(receiver_value, method_name, arg_value)
|
|
1795
1903
|
|
|
1796
1904
|
true
|
|
1797
1905
|
end
|
|
1798
1906
|
|
|
1907
|
+
# #2115 — the result's class name when `**` / `pow` / `<<` / `>>` would produce a number past
|
|
1908
|
+
# `INTEGER_FOLD_BIT_LIMIT`, read from the operands so the huge value is never computed; nil
|
|
1909
|
+
# otherwise. `fold_result` applies the same bound to every other result after the fact.
|
|
1910
|
+
def numeric_blow_up_class(receiver_value, method_name, arg_value)
|
|
1911
|
+
return nil unless arg_value.is_a?(Integer)
|
|
1912
|
+
|
|
1913
|
+
case method_name
|
|
1914
|
+
when :**, :pow then power_blow_up_class(receiver_value, arg_value)
|
|
1915
|
+
when :<<, :>> then "Integer" if shift_blow_up?(receiver_value, method_name, arg_value)
|
|
1916
|
+
end
|
|
1917
|
+
end
|
|
1918
|
+
|
|
1919
|
+
# An Integer base to a negative power is a Rational (`2 ** -3 == (1/8)`); a Rational or Complex
|
|
1920
|
+
# base keeps its class.
|
|
1921
|
+
def power_blow_up_class(base, exponent)
|
|
1922
|
+
return nil unless power_blow_up?(base, exponent)
|
|
1923
|
+
return base.class.name unless base.is_a?(Integer)
|
|
1924
|
+
|
|
1925
|
+
exponent.negative? ? "Rational" : "Integer"
|
|
1926
|
+
end
|
|
1927
|
+
|
|
1928
|
+
# A lower bound on the result's bit length is `floor(log2 |base|) * |exponent|`; past the limit the
|
|
1929
|
+
# result is declined anyway. A Float base (or Float part) stays in Float arithmetic and never grows.
|
|
1930
|
+
def power_blow_up?(base, exponent)
|
|
1931
|
+
bits = power_base_bits(base)
|
|
1932
|
+
!bits.nil? && bits * exponent.abs > INTEGER_FOLD_BIT_LIMIT
|
|
1933
|
+
end
|
|
1934
|
+
|
|
1935
|
+
# `floor(log2 |base|)` for an Integer, the larger of its terms' for a Rational. A Complex part
|
|
1936
|
+
# counts one bit more: `(1+1i) ** n` grows although neither part is past 1.
|
|
1937
|
+
def power_base_bits(base)
|
|
1938
|
+
case base
|
|
1939
|
+
when Integer then base.abs.bit_length - 1
|
|
1940
|
+
when Rational then [base.numerator.abs.bit_length, base.denominator.bit_length].max - 1
|
|
1941
|
+
when Complex then complex_power_base_bits(base)
|
|
1942
|
+
end
|
|
1943
|
+
end
|
|
1944
|
+
|
|
1945
|
+
def complex_power_base_bits(base)
|
|
1946
|
+
parts = [base.real, base.imaginary]
|
|
1947
|
+
return nil unless parts.all? { |part| part.is_a?(Integer) || part.is_a?(Rational) }
|
|
1948
|
+
|
|
1949
|
+
parts.map { |part| power_base_bits(part) + 1 }.max
|
|
1950
|
+
end
|
|
1951
|
+
|
|
1952
|
+
def shift_blow_up?(receiver_value, method_name, arg_value)
|
|
1953
|
+
return false unless receiver_value.is_a?(Integer) && !receiver_value.zero?
|
|
1954
|
+
|
|
1955
|
+
shift = method_name == :<< ? arg_value : -arg_value
|
|
1956
|
+
receiver_value.bit_length + shift > INTEGER_FOLD_BIT_LIMIT
|
|
1957
|
+
end
|
|
1958
|
+
|
|
1799
1959
|
def ops_for(receiver_value)
|
|
1800
1960
|
case receiver_value
|
|
1801
1961
|
when Integer, Float then NUMERIC_BINARY
|
|
@@ -1825,16 +1985,91 @@ module Rigor
|
|
|
1825
1985
|
case method_name
|
|
1826
1986
|
when :+ then string_concat_blow_up?(receiver_value, arg_value)
|
|
1827
1987
|
when :* then string_repeat_blow_up?(receiver_value, arg_value)
|
|
1988
|
+
when :% then format_blow_up?(receiver_value, arg_value)
|
|
1828
1989
|
when :center, :ljust, :rjust then string_pad_blow_up?(arg_value)
|
|
1829
1990
|
else false
|
|
1830
1991
|
end
|
|
1831
1992
|
end
|
|
1832
1993
|
|
|
1994
|
+
# #2125 — the two-argument String folds that can grow past the input: the padded `center` / `ljust`
|
|
1995
|
+
# / `rjust` (width, pad) and `sub` / `gsub` (pattern, replacement).
|
|
1996
|
+
def string_ternary_blow_up?(receiver_value, method_name, av0, av1)
|
|
1997
|
+
return false unless receiver_value.is_a?(String)
|
|
1998
|
+
|
|
1999
|
+
case method_name
|
|
2000
|
+
when :center, :ljust, :rjust then string_pad_blow_up?(av0)
|
|
2001
|
+
when :sub, :gsub then string_substitution_blow_up?(receiver_value, method_name, av1)
|
|
2002
|
+
else false
|
|
2003
|
+
end
|
|
2004
|
+
end
|
|
2005
|
+
|
|
2006
|
+
# An upper bound on a `sub` / `gsub` result with a String replacement: `gsub` matches at most once per
|
|
2007
|
+
# position plus once at the end, and each replacement is the replacement's bytes plus, for every
|
|
2008
|
+
# backslash in it (`\0`, `\1`, `\k<name>`, `` \` ``, `\'`), at most the receiver's bytes again.
|
|
2009
|
+
def string_substitution_blow_up?(receiver_value, method_name, replacement)
|
|
2010
|
+
return false unless replacement.is_a?(String)
|
|
2011
|
+
|
|
2012
|
+
size = receiver_value.bytesize
|
|
2013
|
+
matches = method_name == :gsub ? size + 1 : 1
|
|
2014
|
+
per_match = replacement.bytesize + (replacement.count("\\") * size)
|
|
2015
|
+
size + (matches * per_match) > STRING_FOLD_BYTE_LIMIT
|
|
2016
|
+
end
|
|
2017
|
+
|
|
2018
|
+
# #2125 — `String#%` / `Kernel#format` pad to a field width and render to a precision the format
|
|
2019
|
+
# string spells (`"%02000000000d"`, `"%.100000f"`) or an argument supplies (`"%*d" % [n, 1]`).
|
|
2020
|
+
# Formatting builds the whole padded string before any result gate can look at it — two billion
|
|
2021
|
+
# bytes took 13 s and 4 GB — so the numbers are read from the directives first. A `*` width counts
|
|
2022
|
+
# every Integer argument as a possible width, and a `%%` escape matches as a whole, carrying no digits.
|
|
2023
|
+
FORMAT_DIRECTIVE = /%%|%(?:<[^>]*>|\{[^}]*\}|[-+ 0#*$.\d])*/
|
|
2024
|
+
private_constant :FORMAT_DIRECTIVE
|
|
2025
|
+
|
|
2026
|
+
def format_blow_up?(format_string, arguments)
|
|
2027
|
+
star = false
|
|
2028
|
+
format_string.scan(FORMAT_DIRECTIVE) do |directive|
|
|
2029
|
+
spec = directive.gsub(/<[^>]*>|\{[^}]*\}|\d+\$/, "")
|
|
2030
|
+
return true if spec.scan(/\d+/).any? { |digits| digits.to_i > STRING_FOLD_BYTE_LIMIT }
|
|
2031
|
+
|
|
2032
|
+
star ||= spec.include?("*")
|
|
2033
|
+
end
|
|
2034
|
+
star && format_argument_values(arguments).any? do |value|
|
|
2035
|
+
width = integer_operand(value)
|
|
2036
|
+
!width.nil? && width.abs > STRING_FOLD_BYTE_LIMIT
|
|
2037
|
+
end
|
|
2038
|
+
end
|
|
2039
|
+
|
|
2040
|
+
def format_argument_values(arguments)
|
|
2041
|
+
case arguments
|
|
2042
|
+
when Hash then arguments.values
|
|
2043
|
+
when Array then arguments
|
|
2044
|
+
else [arguments]
|
|
2045
|
+
end
|
|
2046
|
+
end
|
|
2047
|
+
|
|
1833
2048
|
# `"x".center(width)` / `#ljust` / `#rjust` produce a string of `max(width, len)` characters. A
|
|
1834
2049
|
# literal `width` far larger than the receiver would materialise a huge Constant; cap it at the same
|
|
1835
2050
|
# byte limit the concat / repeat paths use.
|
|
1836
2051
|
def string_pad_blow_up?(arg_value)
|
|
1837
|
-
|
|
2052
|
+
width = integer_operand(arg_value)
|
|
2053
|
+
!width.nil? && width > STRING_FOLD_BYTE_LIMIT
|
|
2054
|
+
end
|
|
2055
|
+
|
|
2056
|
+
# The Integer a width or count operand stands for. Ruby converts a Float or Rational one with `NUM2LONG`
|
|
2057
|
+
# (`"x" * 2.9` is `"xx"`, `"x".ljust(2e9)` pads to two billion), and a Complex one whose imaginary part is an
|
|
2058
|
+
# exact zero through its real part (`"x" * Complex(3, 0)` is `"xxx"`; `Complex(3, 0.0)` raises), so the
|
|
2059
|
+
# prechecks read it the same way. Those are every Numeric class a `Constant` carries. A non-finite Float
|
|
2060
|
+
# raises in Ruby, and any other operand is left to the call, so both answer nil.
|
|
2061
|
+
def integer_operand(value)
|
|
2062
|
+
case value
|
|
2063
|
+
when Integer then value
|
|
2064
|
+
when Float then value.finite? ? value.to_i : nil
|
|
2065
|
+
when Rational then value.to_i
|
|
2066
|
+
when Complex then exact_real_operand(value)
|
|
2067
|
+
end
|
|
2068
|
+
end
|
|
2069
|
+
|
|
2070
|
+
def exact_real_operand(value)
|
|
2071
|
+
imaginary = value.imaginary
|
|
2072
|
+
imaginary.is_a?(Integer) && imaginary.zero? ? integer_operand(value.real) : nil
|
|
1838
2073
|
end
|
|
1839
2074
|
|
|
1840
2075
|
def string_concat_blow_up?(receiver_value, arg_value)
|
|
@@ -1843,10 +2078,11 @@ module Rigor
|
|
|
1843
2078
|
end
|
|
1844
2079
|
|
|
1845
2080
|
def string_repeat_blow_up?(receiver_value, arg_value)
|
|
1846
|
-
|
|
1847
|
-
|
|
2081
|
+
# A negative count raises (`ArgumentError`), so the call itself skips that member, as any raising one is.
|
|
2082
|
+
count = integer_operand(arg_value)
|
|
2083
|
+
return false if count.nil? || count.negative?
|
|
1848
2084
|
|
|
1849
|
-
receiver_value.bytesize *
|
|
2085
|
+
receiver_value.bytesize * count > STRING_FOLD_BYTE_LIMIT
|
|
1850
2086
|
end
|
|
1851
2087
|
end
|
|
1852
2088
|
end
|
|
@@ -191,12 +191,16 @@ module Rigor
|
|
|
191
191
|
# Runs the real `Kernel#format` when everything is value-pinned. A malformed directive or an
|
|
192
192
|
# argument-count mismatch raises at fold time; the handler declines so the literal-string
|
|
193
193
|
# lift (or the RBS `String` envelope) answers instead. Results larger than the shared
|
|
194
|
-
# `ConstantFolding::STRING_FOLD_BYTE_LIMIT` decline too, keeping the carrier-size convention
|
|
194
|
+
# `ConstantFolding::STRING_FOLD_BYTE_LIMIT` decline too, keeping the carrier-size convention,
|
|
195
|
+
# and a field width or precision past it declines before the string is built (#2125).
|
|
195
196
|
def fold_format_constant(args)
|
|
196
197
|
return nil unless args.all?(Type::Constant)
|
|
197
198
|
return nil unless args.first.value.is_a?(String)
|
|
198
199
|
|
|
199
|
-
|
|
200
|
+
values = args.drop(1).map(&:value)
|
|
201
|
+
return nil if ConstantFolding.format_blow_up?(args.first.value, values)
|
|
202
|
+
|
|
203
|
+
result = format(args.first.value, *values)
|
|
200
204
|
return nil if result.bytesize > ConstantFolding::STRING_FOLD_BYTE_LIMIT
|
|
201
205
|
|
|
202
206
|
Type::Combinator.constant_of(result)
|
|
@@ -526,12 +526,60 @@ module Rigor
|
|
|
526
526
|
registry = environment&.plugin_registry
|
|
527
527
|
each_source_ancestor_candidate(scope, class_name) do |candidate|
|
|
528
528
|
return nil if scope.discovered_method?(candidate, method_name, kind)
|
|
529
|
-
|
|
530
|
-
|
|
529
|
+
next unless ALLOWED_RBS_COMPLETE_ANCESTORS.include?(candidate) ||
|
|
530
|
+
registry&.rbs_complete_ancestor?(candidate)
|
|
531
|
+
|
|
532
|
+
return shadowed_in_ruby_order?(scope, class_name, kind, method_name, candidate) ? nil : candidate
|
|
531
533
|
end
|
|
532
534
|
nil
|
|
533
535
|
end
|
|
534
536
|
|
|
537
|
+
# Issue #1572 — the walk above is breadth-first, so it reaches the superclass before a module an included
|
|
538
|
+
# module includes, which Ruby's order puts ahead of it (`class P < Rigor::Plugin::Base; include A` with `A`
|
|
539
|
+
# including `M` calls `M#prepare`, not `Base#prepare`), and it passes over RBS-known modules. True when the
|
|
540
|
+
# bridged `ancestor` is not what Ruby calls: on `class_name`'s {Scope::ResolutionChain} in Ruby's order, a
|
|
541
|
+
# project entry ahead of it records the name ({#project_records_name?}), an external entry ahead of it is a
|
|
542
|
+
# module RBS knows and declares the name in, or the chain does not hold it. An RBS-unknown external ahead is
|
|
543
|
+
# passed over, as the breadth-first walk always did. An unsettled chain withholds too: a mixin edge the
|
|
544
|
+
# tables cannot place (a receiver-form `Demo.include(M)` after the body, an edge in a conditional, a block or
|
|
545
|
+
# a hook) may put a definer ahead of the ancestor.
|
|
546
|
+
def shadowed_in_ruby_order?(scope, class_name, kind, method_name, ancestor)
|
|
547
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, kind, :methods)
|
|
548
|
+
return true if chain.unsettled?
|
|
549
|
+
|
|
550
|
+
entries = chain.entries
|
|
551
|
+
stop = entries.index do |entry|
|
|
552
|
+
entry.external? ? entry.candidates.include?(ancestor) : entry.name == ancestor
|
|
553
|
+
end
|
|
554
|
+
return true if stop.nil?
|
|
555
|
+
|
|
556
|
+
entries.first(stop).any? do |entry|
|
|
557
|
+
if entry.external?
|
|
558
|
+
rbs_declares_ahead?(scope, entry, method_name)
|
|
559
|
+
else
|
|
560
|
+
project_records_name?(scope, entry.name, method_name, entry.side)
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Whether the project entry `owner` holds the name on `kind`'s side. `discovered_methods` carries the
|
|
566
|
+
# `attr_*` and `define_method` rows project-wide, but at dispatch it has no row for a `def` in a module
|
|
567
|
+
# declared in another file, so a `def` is also asked of the project-wide def tables. ADR-46:
|
|
568
|
+
# `user_def_for` / `singleton_def_for` file the method edge, positive or negative, so a file that adds or
|
|
569
|
+
# removes the `def` on an entry ahead of the ancestor re-checks the call.
|
|
570
|
+
def project_records_name?(scope, owner, method_name, kind)
|
|
571
|
+
return true if scope.discovered_method?(owner, method_name, kind)
|
|
572
|
+
|
|
573
|
+
return !scope.singleton_def_for(owner, method_name).nil? if kind == :singleton
|
|
574
|
+
|
|
575
|
+
!scope.user_def_for(owner, method_name).nil?
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def rbs_declares_ahead?(scope, entry, method_name)
|
|
579
|
+
entry.candidates.any? { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) } &&
|
|
580
|
+
!Scope::ResolutionChain::Relevance.external_lacks_on?(scope, entry.candidates, entry.side, method_name)
|
|
581
|
+
end
|
|
582
|
+
|
|
535
583
|
# Issue #527 slice 1 — the RBS instance definition a Ruby-source class inherits from a CORE or
|
|
536
584
|
# STDLIB ancestor, or nil. `Oj::EasyHash < Hash` answering `Dynamic[top]` to `has_key?` while
|
|
537
585
|
# `{}.has_key?` folds was the largest single opacity family in the 2026-09-01 corpus sweep.
|
|
@@ -35,43 +35,76 @@ module Rigor
|
|
|
35
35
|
return nil unless SingletonFolding.receiver?(receiver, "Set")
|
|
36
36
|
|
|
37
37
|
case method_name
|
|
38
|
-
when :[] then fold_bracket(args)
|
|
39
|
-
when :new then fold_new(args)
|
|
38
|
+
when :[] then fold_bracket(args, context.call_node)
|
|
39
|
+
when :new then fold_new(args, context.call_node, context.block_type)
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# `Set["a", "b", "c"]` — all positional args must be Constant.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
# `Set["a", "b", "c"]` — all positional args must be Constant to fold to a value. Otherwise the
|
|
44
|
+
# element type is the union of the arguments, widened off their literal pins (#2278).
|
|
45
|
+
def fold_bracket(args, call_node = nil)
|
|
46
|
+
return nil if splatted?(call_node)
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
end
|
|
48
|
+
return Type::Combinator.constant_of(::Set.new(args.map(&:value))) if args.all?(Type::Constant)
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
typed_set(args)
|
|
52
51
|
rescue StandardError
|
|
53
52
|
nil
|
|
54
53
|
end
|
|
55
54
|
|
|
56
|
-
# `Set.new` / `Set.new(
|
|
57
|
-
|
|
55
|
+
# `Set.new` / `Set.new(enum)`. A call with a block declines: the block maps each element
|
|
56
|
+
# (`Set.new([1, 2], &:to_s)` holds `"1"` and `"2"`), so the argument's elements are not the set's.
|
|
57
|
+
def fold_new(args, call_node = nil, block_type = nil)
|
|
58
|
+
return nil if block_type || block_given?(call_node) || splatted?(call_node)
|
|
58
59
|
return Type::Combinator.constant_of(::Set.new) if args.empty?
|
|
59
60
|
return nil if args.size > 1
|
|
60
61
|
|
|
61
62
|
arg = args.first
|
|
62
|
-
|
|
63
|
-
when Type::Tuple
|
|
64
|
-
return nil unless arg.elements.all?(Type::Constant)
|
|
65
|
-
|
|
66
|
-
arg.elements.map(&:value)
|
|
67
|
-
else
|
|
68
|
-
return nil
|
|
69
|
-
end
|
|
63
|
+
return fold_tuple(arg) if arg.is_a?(Type::Tuple)
|
|
70
64
|
|
|
71
|
-
|
|
65
|
+
element = nominal_element(arg)
|
|
66
|
+
element ? typed_set([element]) : nil
|
|
72
67
|
rescue StandardError
|
|
73
68
|
nil
|
|
74
69
|
end
|
|
70
|
+
|
|
71
|
+
def fold_tuple(tuple)
|
|
72
|
+
if tuple.elements.all?(Type::Constant)
|
|
73
|
+
return Type::Combinator.constant_of(::Set.new(tuple.elements.map(&:value)))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
typed_set(tuple.elements)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The element of an `Array[T]` / `Set[T]` argument, or nil for any other carrier.
|
|
80
|
+
def nominal_element(arg)
|
|
81
|
+
return nil unless arg.is_a?(Type::Nominal) && %w[Array Set].include?(arg.class_name)
|
|
82
|
+
return nil unless arg.type_args.size == 1
|
|
83
|
+
|
|
84
|
+
arg.type_args.first
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# `Nominal[Set, [E]]`, `E` the members widened as an `Array.new` fill is (value pins dropped, a literal
|
|
88
|
+
# container widened to its nominal) joined with a `Dynamic[top]` floor. The floor is the conservative half:
|
|
89
|
+
# nothing widens a Set's element when the program mutates it (`add`, `<<`, `merge`, `replace`, `clear`
|
|
90
|
+
# then `<<`, a callee that fills it), so the classes the set was built with are not a claim about what it
|
|
91
|
+
# holds later, and a read of the element must not report on them (#2278).
|
|
92
|
+
def typed_set(types)
|
|
93
|
+
return nil if types.empty?
|
|
94
|
+
|
|
95
|
+
members = types.map { |t| MethodDispatcher.array_new_element(t) }
|
|
96
|
+
Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.union(*members, Type::Combinator.untyped)])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def block_given?(call_node)
|
|
100
|
+
call_node.respond_to?(:block) && !call_node.block.nil?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def splatted?(call_node)
|
|
104
|
+
return false unless call_node.respond_to?(:arguments)
|
|
105
|
+
|
|
106
|
+
(call_node.arguments&.arguments || []).any?(Prism::SplatNode)
|
|
107
|
+
end
|
|
75
108
|
end
|
|
76
109
|
end
|
|
77
110
|
end
|