rigortype 0.4.3 → 0.4.5
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/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -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/mcp_command.rb +1 -1
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- 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 +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- 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 +120 -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 +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- 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 +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -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 +56 -8
- 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 +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- 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 +50 -35
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- 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 +1 -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-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-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-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -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 +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- 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/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +16 -1
|
@@ -190,6 +190,35 @@ 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
|
+
|
|
193
222
|
# Input cartesian product hard cap. Keeps fold cost bounded even when the receiver and argument are
|
|
194
223
|
# both `Union[Constant…]`. 5 × 5 = 25 inputs is permitted; 6 × 6 = 36 is not. The user-facing payoff
|
|
195
224
|
# (a precise small enum) drops off fast past this range and CRuby method invocation cost adds up.
|
|
@@ -649,45 +678,41 @@ module Rigor
|
|
|
649
678
|
def range_constant_unary(range, method_name)
|
|
650
679
|
case method_name
|
|
651
680
|
when :to_a, :entries then range_to_a_tuple(range)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
681
|
+
# #2115 — `Range#min` / `#max` / `#minmax` / `#size` are O(1) on Integer endpoints in CRuby, so
|
|
682
|
+
# none of these materialises the range. The no-arg `first` / `last` read the endpoints, not the
|
|
683
|
+
# elements: `first` is `begin` and `last` is `end` whether the range is empty, reversed or
|
|
684
|
+
# exclusive (`(5..1).first` is 5, `(1...10).last` is 10). Only `min` / `max` depend on emptiness
|
|
685
|
+
# and exclusivity (`(5..1).min` is nil, `(1...10).max` is 9).
|
|
686
|
+
when :first then Type::Combinator.constant_of(range.begin)
|
|
687
|
+
when :last then Type::Combinator.constant_of(range.end)
|
|
688
|
+
when :min then Type::Combinator.constant_of(range.min)
|
|
689
|
+
when :max then Type::Combinator.constant_of(range.max)
|
|
690
|
+
when :count, :size, :length then Type::Combinator.constant_of(range.size)
|
|
691
|
+
when :minmax then Type::Combinator.tuple_of(*range.minmax.map { |v| Type::Combinator.constant_of(v) })
|
|
656
692
|
# `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
|
|
693
|
+
# yields a single Integer — no materialisation; only `INTEGER_FOLD_BIT_LIMIT` applies. Endless
|
|
694
|
+
# ranges are already excluded by the Integer-endpoint guard in the caller.
|
|
695
|
+
when :sum then range_sum_constant(range)
|
|
660
696
|
end
|
|
661
697
|
end
|
|
662
698
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
699
|
+
# The Gauss sum of a range of large endpoints can still pass `INTEGER_FOLD_BIT_LIMIT`; it is then
|
|
700
|
+
# an `Integer` (both endpoints are).
|
|
701
|
+
def range_sum_constant(range)
|
|
702
|
+
sum = range.sum
|
|
703
|
+
return Type::Combinator.nominal_of("Integer") if oversized_number?(sum)
|
|
667
704
|
|
|
668
|
-
Type::Combinator.
|
|
705
|
+
Type::Combinator.constant_of(sum)
|
|
669
706
|
end
|
|
670
707
|
|
|
671
|
-
def
|
|
672
|
-
|
|
673
|
-
return
|
|
674
|
-
|
|
675
|
-
Type::Combinator.constant_of(edge == :first ? values.first : values.last)
|
|
676
|
-
end
|
|
708
|
+
def range_to_a_tuple(range)
|
|
709
|
+
# `Range#size` is O(1) on Integer endpoints: check the cardinality before materialising.
|
|
710
|
+
return nil if range.size > RANGE_TO_A_LIMIT
|
|
677
711
|
|
|
678
|
-
def range_minmax_tuple(range)
|
|
679
712
|
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
|
|
713
|
+
return Type::Combinator.tuple_of if values.empty?
|
|
686
714
|
|
|
687
|
-
Type::Combinator.tuple_of(
|
|
688
|
-
Type::Combinator.constant_of(values.first),
|
|
689
|
-
Type::Combinator.constant_of(values.last)
|
|
690
|
-
)
|
|
715
|
+
Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
|
|
691
716
|
end
|
|
692
717
|
|
|
693
718
|
# `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head / tail forms. `first`/`last` already
|
|
@@ -1175,6 +1200,17 @@ module Rigor
|
|
|
1175
1200
|
# "Integer-ness" was already implied by the inputs (so the widening fallback only fires for
|
|
1176
1201
|
# arithmetic over integers).
|
|
1177
1202
|
def build_constant_type(values, source: nil)
|
|
1203
|
+
widened, values = values.partition { |value| value.is_a?(WidenedResult) }
|
|
1204
|
+
return build_scalar_constant_type(values, source) if widened.empty?
|
|
1205
|
+
|
|
1206
|
+
nominals = widened.map(&:class_name).uniq.map { |name| Type::Combinator.nominal_of(name) }
|
|
1207
|
+
return Type::Combinator.union(*nominals) if values.empty?
|
|
1208
|
+
|
|
1209
|
+
constants = build_scalar_constant_type(values, source)
|
|
1210
|
+
constants && Type::Combinator.union(constants, *nominals)
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
def build_scalar_constant_type(values, source)
|
|
1178
1214
|
return nil if values.empty?
|
|
1179
1215
|
|
|
1180
1216
|
unique = values.uniq
|
|
@@ -1607,9 +1643,12 @@ module Rigor
|
|
|
1607
1643
|
# `flat_map`.
|
|
1608
1644
|
def invoke_binary(receiver_value, method_name, arg_value)
|
|
1609
1645
|
return nil unless safe?(receiver_value, method_name, arg_value)
|
|
1646
|
+
return nil if range_enumeration_blow_up?(receiver_value, method_name)
|
|
1610
1647
|
|
|
1611
|
-
|
|
1612
|
-
|
|
1648
|
+
widened = numeric_blow_up_class(receiver_value, method_name, arg_value)
|
|
1649
|
+
return [WidenedResult.new(widened)] if widened
|
|
1650
|
+
|
|
1651
|
+
fold_result(receiver_value.public_send(method_name, arg_value))
|
|
1613
1652
|
rescue StandardError
|
|
1614
1653
|
nil
|
|
1615
1654
|
end
|
|
@@ -1620,8 +1659,7 @@ module Rigor
|
|
|
1620
1659
|
def invoke_ternary(receiver_value, method_name, av0, av1)
|
|
1621
1660
|
return nil unless ternary_method_allowed?(receiver_value, method_name)
|
|
1622
1661
|
|
|
1623
|
-
|
|
1624
|
-
foldable_constant_value?(result) ? [result] : nil
|
|
1662
|
+
fold_result(receiver_value.public_send(method_name, av0, av1))
|
|
1625
1663
|
rescue StandardError
|
|
1626
1664
|
nil
|
|
1627
1665
|
end
|
|
@@ -1630,13 +1668,21 @@ module Rigor
|
|
|
1630
1668
|
def invoke_unary(receiver_value, method_name)
|
|
1631
1669
|
return nil unless unary_safe?(receiver_value, method_name)
|
|
1632
1670
|
return nil if string_unary_blow_up?(receiver_value, method_name)
|
|
1671
|
+
return nil if range_enumeration_blow_up?(receiver_value, method_name)
|
|
1633
1672
|
|
|
1634
|
-
|
|
1635
|
-
foldable_constant_value?(result) ? [result] : nil
|
|
1673
|
+
fold_result(receiver_value.public_send(method_name))
|
|
1636
1674
|
rescue StandardError
|
|
1637
1675
|
nil
|
|
1638
1676
|
end
|
|
1639
1677
|
|
|
1678
|
+
# `[result]` for a foldable value, `[WidenedResult]` for a number past `INTEGER_FOLD_BIT_LIMIT`, nil
|
|
1679
|
+
# for anything else.
|
|
1680
|
+
def fold_result(result)
|
|
1681
|
+
return [WidenedResult.new(result.class.name)] if oversized_number?(result)
|
|
1682
|
+
|
|
1683
|
+
foldable_constant_value?(result) ? [result] : nil
|
|
1684
|
+
end
|
|
1685
|
+
|
|
1640
1686
|
def unary_safe?(receiver_value, method_name)
|
|
1641
1687
|
return true if unary_ops_for(receiver_value).include?(method_name)
|
|
1642
1688
|
|
|
@@ -1775,7 +1821,44 @@ module Rigor
|
|
|
1775
1821
|
# with the widened class.
|
|
1776
1822
|
return false if value.is_a?(Float) && value.nan?
|
|
1777
1823
|
return false if value.is_a?(Complex) && complex_nan?(value)
|
|
1824
|
+
return false if oversized_number?(value)
|
|
1825
|
+
|
|
1826
|
+
true
|
|
1827
|
+
end
|
|
1828
|
+
|
|
1829
|
+
# True when an Integer, or an Integer part of a Rational / Complex, is past `INTEGER_FOLD_BIT_LIMIT`.
|
|
1830
|
+
def oversized_number?(value)
|
|
1831
|
+
case value
|
|
1832
|
+
when Integer then value.bit_length > INTEGER_FOLD_BIT_LIMIT
|
|
1833
|
+
when Rational then oversized_number?(value.numerator) || oversized_number?(value.denominator)
|
|
1834
|
+
when Complex then oversized_number?(value.real) || oversized_number?(value.imaginary)
|
|
1835
|
+
else false
|
|
1836
|
+
end
|
|
1837
|
+
end
|
|
1838
|
+
|
|
1839
|
+
# #2115 — true when `receiver_value` is a `Range` the method would walk element by element and the
|
|
1840
|
+
# range holds more than `RANGE_ENUMERATION_LIMIT` elements (an endless one never stops).
|
|
1841
|
+
def range_enumeration_blow_up?(receiver_value, method_name)
|
|
1842
|
+
return false unless receiver_value.is_a?(Range)
|
|
1843
|
+
return false if RANGE_ENDPOINT_METHODS.include?(method_name)
|
|
1844
|
+
return false if RANGE_COVER_METHODS.include?(method_name) && numeric_range?(receiver_value)
|
|
1845
|
+
|
|
1846
|
+
!range_within_enumeration_budget?(receiver_value)
|
|
1847
|
+
end
|
|
1778
1848
|
|
|
1849
|
+
def numeric_range?(range)
|
|
1850
|
+
range.begin.is_a?(Numeric) || range.end.is_a?(Numeric)
|
|
1851
|
+
end
|
|
1852
|
+
|
|
1853
|
+
# Takes at most `RANGE_ENUMERATION_LIMIT + 1` elements (`Range#first(n)` stops there, endless or
|
|
1854
|
+
# not). A range that cannot iterate at all (a Float or Time begin) raises, and so does every
|
|
1855
|
+
# enumerating method on it before doing any work, so it counts as within budget: the invoke's own
|
|
1856
|
+
# rescue declines an enumerating fold, and `(t0..t1).include?(t)`, which CRuby answers by `cover?`,
|
|
1857
|
+
# keeps folding. A beginless range raises `RangeError` here, which the invoke's own rescue turns into
|
|
1858
|
+
# a declined fold, as the enumerating method itself would raise.
|
|
1859
|
+
def range_within_enumeration_budget?(range)
|
|
1860
|
+
range.first(RANGE_ENUMERATION_LIMIT + 1).size <= RANGE_ENUMERATION_LIMIT
|
|
1861
|
+
rescue TypeError
|
|
1779
1862
|
true
|
|
1780
1863
|
end
|
|
1781
1864
|
|
|
@@ -1796,6 +1879,58 @@ module Rigor
|
|
|
1796
1879
|
true
|
|
1797
1880
|
end
|
|
1798
1881
|
|
|
1882
|
+
# #2115 — the result's class name when `**` / `pow` / `<<` / `>>` would produce a number past
|
|
1883
|
+
# `INTEGER_FOLD_BIT_LIMIT`, read from the operands so the huge value is never computed; nil
|
|
1884
|
+
# otherwise. `fold_result` applies the same bound to every other result after the fact.
|
|
1885
|
+
def numeric_blow_up_class(receiver_value, method_name, arg_value)
|
|
1886
|
+
return nil unless arg_value.is_a?(Integer)
|
|
1887
|
+
|
|
1888
|
+
case method_name
|
|
1889
|
+
when :**, :pow then power_blow_up_class(receiver_value, arg_value)
|
|
1890
|
+
when :<<, :>> then "Integer" if shift_blow_up?(receiver_value, method_name, arg_value)
|
|
1891
|
+
end
|
|
1892
|
+
end
|
|
1893
|
+
|
|
1894
|
+
# An Integer base to a negative power is a Rational (`2 ** -3 == (1/8)`); a Rational or Complex
|
|
1895
|
+
# base keeps its class.
|
|
1896
|
+
def power_blow_up_class(base, exponent)
|
|
1897
|
+
return nil unless power_blow_up?(base, exponent)
|
|
1898
|
+
return base.class.name unless base.is_a?(Integer)
|
|
1899
|
+
|
|
1900
|
+
exponent.negative? ? "Rational" : "Integer"
|
|
1901
|
+
end
|
|
1902
|
+
|
|
1903
|
+
# A lower bound on the result's bit length is `floor(log2 |base|) * |exponent|`; past the limit the
|
|
1904
|
+
# result is declined anyway. A Float base (or Float part) stays in Float arithmetic and never grows.
|
|
1905
|
+
def power_blow_up?(base, exponent)
|
|
1906
|
+
bits = power_base_bits(base)
|
|
1907
|
+
!bits.nil? && bits * exponent.abs > INTEGER_FOLD_BIT_LIMIT
|
|
1908
|
+
end
|
|
1909
|
+
|
|
1910
|
+
# `floor(log2 |base|)` for an Integer, the larger of its terms' for a Rational. A Complex part
|
|
1911
|
+
# counts one bit more: `(1+1i) ** n` grows although neither part is past 1.
|
|
1912
|
+
def power_base_bits(base)
|
|
1913
|
+
case base
|
|
1914
|
+
when Integer then base.abs.bit_length - 1
|
|
1915
|
+
when Rational then [base.numerator.abs.bit_length, base.denominator.bit_length].max - 1
|
|
1916
|
+
when Complex then complex_power_base_bits(base)
|
|
1917
|
+
end
|
|
1918
|
+
end
|
|
1919
|
+
|
|
1920
|
+
def complex_power_base_bits(base)
|
|
1921
|
+
parts = [base.real, base.imaginary]
|
|
1922
|
+
return nil unless parts.all? { |part| part.is_a?(Integer) || part.is_a?(Rational) }
|
|
1923
|
+
|
|
1924
|
+
parts.map { |part| power_base_bits(part) + 1 }.max
|
|
1925
|
+
end
|
|
1926
|
+
|
|
1927
|
+
def shift_blow_up?(receiver_value, method_name, arg_value)
|
|
1928
|
+
return false unless receiver_value.is_a?(Integer) && !receiver_value.zero?
|
|
1929
|
+
|
|
1930
|
+
shift = method_name == :<< ? arg_value : -arg_value
|
|
1931
|
+
receiver_value.bit_length + shift > INTEGER_FOLD_BIT_LIMIT
|
|
1932
|
+
end
|
|
1933
|
+
|
|
1799
1934
|
def ops_for(receiver_value)
|
|
1800
1935
|
case receiver_value
|
|
1801
1936
|
when Integer, Float then NUMERIC_BINARY
|
|
@@ -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.
|
|
@@ -220,7 +220,8 @@ module Rigor
|
|
|
220
220
|
# `call.undefined-method`.
|
|
221
221
|
patched_result =
|
|
222
222
|
unless skip_project_patched
|
|
223
|
-
try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment
|
|
223
|
+
try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment,
|
|
224
|
+
call_node: call_node, scope: scope)
|
|
224
225
|
end
|
|
225
226
|
if patched_result
|
|
226
227
|
scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
|
|
@@ -258,6 +259,22 @@ module Rigor
|
|
|
258
259
|
return discovered_result
|
|
259
260
|
end
|
|
260
261
|
|
|
262
|
+
# Issue #1708 — a `pre_eval:` patch on one of the receiver's ancestors (`class Object; def zz`,
|
|
263
|
+
# `module Enumerable; def sum_by`). Below the RBS tier, whose decline means no nearer RBS declares the
|
|
264
|
+
# method, and below the tiers that answer the receiver's own patch or a gem catalogue entry. A nearer
|
|
265
|
+
# `def` in the project's source (on an RBS-known receiver, which the discovered-method tier leaves to
|
|
266
|
+
# user-method inference) stops the walk: see {#project_source_defines?}. That probe reads the scope's def
|
|
267
|
+
# index, so a single-file scope (`type-of`, `annotate`, an LSP buffer) cannot see a `def` in another file.
|
|
268
|
+
ancestor_patched =
|
|
269
|
+
if !skip_project_patched && scope
|
|
270
|
+
try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment,
|
|
271
|
+
call_node: call_node, scope: scope, through_ancestors: true)
|
|
272
|
+
end
|
|
273
|
+
if ancestor_patched
|
|
274
|
+
scope&.record_dynamic_origin(call_node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
|
|
275
|
+
return ancestor_patched
|
|
276
|
+
end
|
|
277
|
+
|
|
261
278
|
# ADR-5 robustness — synthesized-stub-type tier. When the receiver is a type Rigor
|
|
262
279
|
# invented to make an otherwise-unbuildable project signature resolve (a
|
|
263
280
|
# missing-namespace module, or a stub for a referenced-but-undeclared type like an
|
|
@@ -644,7 +661,13 @@ module Rigor
|
|
|
644
661
|
# `def to_url; "hello"; end` patched onto `String` now resolves `s.to_url` to
|
|
645
662
|
# `Dynamic[Nominal[String]]` instead of the pre-3a `Dynamic[Top]`. Falls back to
|
|
646
663
|
# `Dynamic[Top]` when the heuristic declined (non-literal tail expression).
|
|
647
|
-
|
|
664
|
+
#
|
|
665
|
+
# `through_ancestors:` (issue #1708), with a scope, searches the receiver's ancestors as well, through
|
|
666
|
+
# {ProjectPatchedMethods#lookup_through_ancestors}, stopping at a nearer project-source `def`. A private or
|
|
667
|
+
# protected patch Ruby would not let the call reach ({ProjectPatchedMethods#callable?}) answers nothing, so
|
|
668
|
+
# `1.kp` on `module Kernel; private def kp` reads as the undefined call it is.
|
|
669
|
+
def try_project_patched_method(receiver_type, method_name, arg_types, block_type, environment, # rubocop:disable Metrics/ParameterLists
|
|
670
|
+
call_node: nil, scope: nil, through_ancestors: false)
|
|
648
671
|
registry = environment&.project_patched_methods
|
|
649
672
|
return nil if registry.nil? || registry.empty?
|
|
650
673
|
|
|
@@ -652,14 +675,55 @@ module Rigor
|
|
|
652
675
|
return nil if class_name.nil?
|
|
653
676
|
|
|
654
677
|
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
655
|
-
entry = registry
|
|
678
|
+
entry = project_patched_entry(registry, class_name, method_name, kind, environment,
|
|
679
|
+
scope: scope, through_ancestors: through_ancestors)
|
|
656
680
|
return nil if entry.nil?
|
|
681
|
+
|
|
682
|
+
verdict = registry.callable?(entry, explicit: explicit_non_self_receiver?(call_node),
|
|
683
|
+
self_type: scope ? scope.self_type : :unknown,
|
|
684
|
+
rbs_loader: environment.rbs_loader)
|
|
685
|
+
return nil if verdict == false
|
|
686
|
+
return Type::Combinator.untyped if verdict.nil?
|
|
657
687
|
return Type::Combinator.dynamic(entry.return_type) if entry.return_type
|
|
658
688
|
|
|
659
689
|
aliased = project_patched_alias_target(entry, receiver_type, arg_types, block_type, environment)
|
|
660
690
|
aliased ? Type::Combinator.dynamic(aliased) : Type::Combinator.untyped
|
|
661
691
|
end
|
|
662
692
|
|
|
693
|
+
def project_patched_entry(registry, class_name, method_name, kind, environment, scope:, through_ancestors:)
|
|
694
|
+
return registry.lookup(class_name: class_name, method_name: method_name, kind: kind) unless through_ancestors
|
|
695
|
+
|
|
696
|
+
registry.lookup_through_ancestors(
|
|
697
|
+
class_name: class_name, method_name: method_name, kind: kind, rbs_loader: environment.rbs_loader,
|
|
698
|
+
source_defines: ->(owner, side) { project_source_defines?(scope, owner, method_name, side) }
|
|
699
|
+
)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Issue #1708 — does the project's own source define `method_name` on `owner` (`side` as the ancestor walk
|
|
703
|
+
# reaches it), or on a module the project prepends to or includes into it? Ruby runs that `def` ahead of a
|
|
704
|
+
# patch on a farther ancestor: `class Integer; def nn = "s"` beats `class Object; def nn = nil` in a
|
|
705
|
+
# `pre_eval:` file. The owner's level of its {Scope::ResolutionChain} (its prepends, itself, its includes
|
|
706
|
+
# and their own mixins, or a class object's singleton and its extends) holds those definers; whether one
|
|
707
|
+
# of them defines the name does not depend on their order, so the chain is searched without settling it.
|
|
708
|
+
def project_source_defines?(scope, owner, method_name, side)
|
|
709
|
+
return false unless scope.known_user_class?(owner)
|
|
710
|
+
|
|
711
|
+
chain = Scope::ResolutionChain.for(scope, owner, side, :methods)
|
|
712
|
+
own_level_end = chain.level_starts[1] || chain.entries.size
|
|
713
|
+
found = chain.search(scope, 0, own_level_end) do |entry|
|
|
714
|
+
!entry.external? && source_def?(scope, entry.name, method_name, entry.side)
|
|
715
|
+
end
|
|
716
|
+
!found.nil?
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def source_def?(scope, owner, method_name, side)
|
|
720
|
+
if side == :singleton
|
|
721
|
+
scope.discovered_method?(owner, method_name, :singleton) || !scope.singleton_def_for(owner, method_name).nil?
|
|
722
|
+
else
|
|
723
|
+
scope.discovered_method?(owner, method_name, :instance) || !scope.user_def_for(owner, method_name).nil?
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
663
727
|
# Issue #1702 — an alias in a `pre_eval:` patch whose old name no earlier patch binding defines
|
|
664
728
|
# (`alias old_plus +` on Integer, or `alias_method :orig_succ, :succ` ahead of the patch's own `def succ`)
|
|
665
729
|
# names the method the class had before the patch, so it answers what that method answers for the same
|