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
|
@@ -10,6 +10,7 @@ require_relative "../rbs_extended"
|
|
|
10
10
|
require_relative "../analysis/fact_store"
|
|
11
11
|
require_relative "../builtins/regex_refinement"
|
|
12
12
|
require_relative "guard_rebinding"
|
|
13
|
+
require_relative "intervening_ivar_reset"
|
|
13
14
|
require_relative "key_presence_guard"
|
|
14
15
|
require_relative "last_line"
|
|
15
16
|
require_relative "operand_effects"
|
|
@@ -511,8 +512,9 @@ module Rigor
|
|
|
511
512
|
# none of the condition shapes are recognised, both returned scopes equal the input
|
|
512
513
|
# scope. The catalogue mirrors {.case_equality_target_class}: static class/module
|
|
513
514
|
# constants narrow as `is_a?`; integer/float-endpoint ranges narrow to `Numeric`;
|
|
514
|
-
# string-endpoint ranges
|
|
515
|
-
# `false` literal narrows as its
|
|
515
|
+
# string-endpoint ranges narrow to `String`; regexp literals narrow as
|
|
516
|
+
# {.regexp_case_equality_type} says; a `nil`, `true` or `false` literal narrows as its
|
|
517
|
+
# class does ({.when_condition_class}).
|
|
516
518
|
def case_when_scopes(subject, conditions, scope)
|
|
517
519
|
# C1 — `case x when /re/` runs `/re/ === x`, which sets the regex match-data globals
|
|
518
520
|
# exactly as a successful `=~` does. Narrow `$~`/`$&`/`$1..$N` on the clause body (the
|
|
@@ -522,7 +524,7 @@ module Rigor
|
|
|
522
524
|
|
|
523
525
|
# Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does, and so
|
|
524
526
|
# does an instance variable (#1446).
|
|
525
|
-
slot =
|
|
527
|
+
slot = case_subject_slot(subject, scope)
|
|
526
528
|
return [body_scope, scope] if slot.nil?
|
|
527
529
|
|
|
528
530
|
truthy_type, falsey_type = case_when_types(scope, slot.current, conditions)
|
|
@@ -536,6 +538,32 @@ module Rigor
|
|
|
536
538
|
CASE_SUBJECT_KINDS = %i[local ivar global constant].freeze
|
|
537
539
|
private_constant :CASE_SUBJECT_KINDS
|
|
538
540
|
|
|
541
|
+
# The slot a `case` subject narrows. Issue #2211 — a subject that writes a local or instance variable
|
|
542
|
+
# (`case (x = v)`, `case x = v`) tests the value it stores, which the variable holds in every clause, so the
|
|
543
|
+
# clauses narrow the variable as they narrow a bare read of it: `case (x = s ? String(s) : nil) when String then
|
|
544
|
+
# x.upcase end` reads `x` as `String`. A single-statement `(…)` is unwrapped.
|
|
545
|
+
def case_subject_slot(subject, scope)
|
|
546
|
+
subject = unwrap_case_subject(subject)
|
|
547
|
+
case subject
|
|
548
|
+
when Prism::LocalVariableWriteNode
|
|
549
|
+
variable_slot(:local, subject.name, scope.local(subject.name), CASE_SUBJECT_KINDS)
|
|
550
|
+
when Prism::InstanceVariableWriteNode
|
|
551
|
+
variable_slot(:ivar, subject.name, scope.ivar(subject.name), CASE_SUBJECT_KINDS)
|
|
552
|
+
else receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
private_class_method :case_subject_slot
|
|
556
|
+
|
|
557
|
+
# `subject` with each single-statement `(…)` around it removed: the node {.case_subject_slot} narrows through,
|
|
558
|
+
# which `StatementEvaluator#when_test_rebinds_subject?` reads the same way.
|
|
559
|
+
def unwrap_case_subject(subject)
|
|
560
|
+
while subject.is_a?(Prism::ParenthesesNode) && subject.body.is_a?(Prism::StatementsNode) &&
|
|
561
|
+
subject.body.body.size == 1
|
|
562
|
+
subject = subject.body.body.first
|
|
563
|
+
end
|
|
564
|
+
subject
|
|
565
|
+
end
|
|
566
|
+
|
|
539
567
|
# Returns `[body_scope, falsey_scope]` for one clause of a `case` with a subject: a `when` clause narrows
|
|
540
568
|
# through {.case_when_scopes}, and an `in` clause only for a bare class pattern ({.bare_class_pattern_node}),
|
|
541
569
|
# which matches exactly when `subject.is_a?(C)`. Every other pattern can fail to match where a class test
|
|
@@ -1797,9 +1825,13 @@ module Rigor
|
|
|
1797
1825
|
# passes through unchanged (failing one pattern proves nothing about the string's shape).
|
|
1798
1826
|
# A non-literal / unanchored / `\Z` / line-anchored / `//x` pattern falls through to no
|
|
1799
1827
|
# narrowing.
|
|
1828
|
+
#
|
|
1829
|
+
# The refinement names a String, so it shares `=~`'s gate (#1955): a receiver that may be a Symbol, or an
|
|
1830
|
+
# untyped one, is left as it is ({#refinable_operand?}).
|
|
1800
1831
|
def analyse_whole_regex_match_predicate(node, scope)
|
|
1801
1832
|
return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
1802
1833
|
return nil if node.arguments.nil? || node.arguments.arguments.size != 1
|
|
1834
|
+
return nil unless refinable_operand?(node, scope)
|
|
1803
1835
|
|
|
1804
1836
|
pattern = regex_operand_pattern(node.arguments.arguments.first, scope)
|
|
1805
1837
|
return nil unless pattern.is_a?(RegexMatchPattern)
|
|
@@ -2636,10 +2668,10 @@ module Rigor
|
|
|
2636
2668
|
# iff the endpoints compare it. We conservatively narrow `x` to `Numeric` for
|
|
2637
2669
|
# integer-endpoint ranges and to `String` for string-endpoint ranges; other endpoint
|
|
2638
2670
|
# types fall through.
|
|
2639
|
-
# - **Regexp literal receiver** (`/foo/ === x`): the
|
|
2640
|
-
#
|
|
2641
|
-
# type unchanged because `Regexp#===` returns false for non-Strings AND
|
|
2642
|
-
# that simply do not match.
|
|
2671
|
+
# - **Regexp literal receiver** (`/foo/ === x`): the truthy edge narrows `x` as
|
|
2672
|
+
# {#regexp_case_equality_type} says (its String and Symbol parts, #1955). The falsey edge
|
|
2673
|
+
# keeps the entry type unchanged because `Regexp#===` returns false for non-Strings AND
|
|
2674
|
+
# for Strings that simply do not match.
|
|
2643
2675
|
#
|
|
2644
2676
|
# Anything else — non-local LHS argument, dynamic receiver, custom `===` method — falls
|
|
2645
2677
|
# through to the no-narrowing branch (nil), preserving the entry scope on both edges.
|
|
@@ -2703,6 +2735,9 @@ module Rigor
|
|
|
2703
2735
|
if (class_name = lexical_class_name(receiver, scope))
|
|
2704
2736
|
return class_predicate_scopes(scope, local_name, current, class_name, exact: false)
|
|
2705
2737
|
end
|
|
2738
|
+
if regexp_literal?(receiver)
|
|
2739
|
+
return [scope.with_local(local_name, regexp_case_equality_type(current, scope)), scope]
|
|
2740
|
+
end
|
|
2706
2741
|
|
|
2707
2742
|
target_class = case_equality_target_class(receiver)
|
|
2708
2743
|
return nil if target_class.nil?
|
|
@@ -2716,7 +2751,7 @@ module Rigor
|
|
|
2716
2751
|
end
|
|
2717
2752
|
|
|
2718
2753
|
# The class a `when` condition narrows the subject to, as `is_a?` would, or nil when the condition is no class
|
|
2719
|
-
# test Rigor reads: a class or module constant, a range
|
|
2754
|
+
# test Rigor reads: a class or module constant, a range literal ({.case_equality_target_class}),
|
|
2720
2755
|
# or (#1925) a `nil` / `true` / `false` literal. `nil === x` runs `NilClass#===`, which holds only for `nil`
|
|
2721
2756
|
# itself whatever the subject's class defines, and `nil` is `NilClass`'s only instance, so `when nil` is
|
|
2722
2757
|
# `when NilClass`; `true` and `false` likewise.
|
|
@@ -2733,8 +2768,9 @@ module Rigor
|
|
|
2733
2768
|
# Maps a case-equality literal receiver to the class whose membership is implied by the
|
|
2734
2769
|
# truthy edge. `Prism::ParenthesesNode` wrappers are transparently unwrapped (`(1..10)
|
|
2735
2770
|
# === x` is parsed with the range inside parentheses). Range literals: integer
|
|
2736
|
-
# endpoints → `Numeric`; string endpoints → `String`. Regexp literals → `String
|
|
2737
|
-
#
|
|
2771
|
+
# endpoints → `Numeric`; string endpoints → `String`. Regexp literals → `String`, read only
|
|
2772
|
+
# as the class a dead `when` arm names: their narrowing is {#regexp_case_equality_type}'s,
|
|
2773
|
+
# which the callers consult first. Other shapes return nil so the caller falls through.
|
|
2738
2774
|
def case_equality_target_class(receiver)
|
|
2739
2775
|
receiver = unwrap_parens(receiver)
|
|
2740
2776
|
case receiver
|
|
@@ -2743,6 +2779,68 @@ module Rigor
|
|
|
2743
2779
|
end
|
|
2744
2780
|
end
|
|
2745
2781
|
|
|
2782
|
+
def regexp_literal?(node)
|
|
2783
|
+
case unwrap_parens(node)
|
|
2784
|
+
when Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode then true
|
|
2785
|
+
else false
|
|
2786
|
+
end
|
|
2787
|
+
end
|
|
2788
|
+
|
|
2789
|
+
# Issue #1955 — the subject on the match edge of `/re/ === x` (and of `case x when /re/`). `Regexp#===`
|
|
2790
|
+
# matches a String, a Symbol by its name, and an object that converts with `to_str`, and answers false for
|
|
2791
|
+
# anything else. A member is kept whole when its class defines `to_str` (#2002) — a String and its
|
|
2792
|
+
# subclasses, a class whose signature, ancestry or project source declares it — or when no signature
|
|
2793
|
+
# declares the class, and a `Dynamic` member is kept, since it may also be a Symbol at run time. Any other
|
|
2794
|
+
# member narrows to its String and Symbol parts, which are only what can match: a `String | nil` subject
|
|
2795
|
+
# reads `String`, a Symbol one stays `Symbol`, an Integer one drops out. An `Object`, `BasicObject` or
|
|
2796
|
+
# module member narrows too (to `String | Symbol`): its instances may be anything, and reading the edge as
|
|
2797
|
+
# the String it most often is keeps `s.length` on it from reporting. The no-match edge keeps the entry type:
|
|
2798
|
+
# a String that does not match reaches it too.
|
|
2799
|
+
def regexp_case_equality_type(current, scope)
|
|
2800
|
+
members = current.is_a?(Type::Union) ? current.members : [current]
|
|
2801
|
+
Type::Combinator.union(*members.map { |member| regexp_case_equality_member(member, scope) })
|
|
2802
|
+
end
|
|
2803
|
+
|
|
2804
|
+
def regexp_case_equality_member(member, scope)
|
|
2805
|
+
return member if member.is_a?(Type::Dynamic)
|
|
2806
|
+
return member if converts_with_to_str?(member, scope)
|
|
2807
|
+
|
|
2808
|
+
parts = %w[String Symbol].map do |class_name|
|
|
2809
|
+
narrow_class(member, class_name, exact: false, environment: scope.environment, scope: scope)
|
|
2810
|
+
end
|
|
2811
|
+
Type::Combinator.union(*parts)
|
|
2812
|
+
end
|
|
2813
|
+
|
|
2814
|
+
# True when a class `member` dispatches through declares `to_str`, or is declared by no signature
|
|
2815
|
+
# ({#class_lacks_method?} answers false for it). `Object`, `BasicObject` and a module String or Symbol
|
|
2816
|
+
# includes (`Kernel`, `Comparable`) are read as their String and Symbol parts instead: their instances may be
|
|
2817
|
+
# anything, and the String they most often are keeps a String method on the arm from reporting. Any other
|
|
2818
|
+
# module is kept whole, since a matching value may include it; so is one of those that declares `to_str`.
|
|
2819
|
+
def converts_with_to_str?(member, scope)
|
|
2820
|
+
targets = ProjectMethodOwnership.targets(member)
|
|
2821
|
+
return false if targets.nil? || targets.empty?
|
|
2822
|
+
|
|
2823
|
+
targets.any? { |class_name, kind| class_converts_with_to_str?(class_name, kind, scope) }
|
|
2824
|
+
end
|
|
2825
|
+
|
|
2826
|
+
# The modules String's or Symbol's own ancestry includes, read from the running Ruby.
|
|
2827
|
+
STRING_SYMBOL_MODULES = (String.ancestors + Symbol.ancestors).grep_v(Class).filter_map(&:name).uniq.freeze
|
|
2828
|
+
private_constant :STRING_SYMBOL_MODULES
|
|
2829
|
+
|
|
2830
|
+
def class_converts_with_to_str?(class_name, kind, scope)
|
|
2831
|
+
return false if kind == :instance && OPEN_SURFACE_CLASSES.include?(class_name)
|
|
2832
|
+
if kind == :instance && scope.environment.rbs_module?(class_name)
|
|
2833
|
+
return module_declares_to_str?(class_name, scope) || !STRING_SYMBOL_MODULES.include?(class_name)
|
|
2834
|
+
end
|
|
2835
|
+
|
|
2836
|
+
!class_lacks_method?(class_name, :to_str, kind, scope)
|
|
2837
|
+
end
|
|
2838
|
+
|
|
2839
|
+
def module_declares_to_str?(module_name, scope)
|
|
2840
|
+
ProjectMethodOwnership.defines?(module_name, :to_str, :instance, scope) ||
|
|
2841
|
+
!ExternalAncestorResolution.method_definition(module_name, :to_str, :instance, scope: scope).nil?
|
|
2842
|
+
end
|
|
2843
|
+
|
|
2746
2844
|
def unwrap_parens(node)
|
|
2747
2845
|
while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode) &&
|
|
2748
2846
|
node.body.body.size == 1
|
|
@@ -2971,6 +3069,10 @@ module Rigor
|
|
|
2971
3069
|
return integer_literal_when_result(current, int_literal, falsey_acc)
|
|
2972
3070
|
end
|
|
2973
3071
|
|
|
3072
|
+
if regexp_literal?(condition)
|
|
3073
|
+
return { truthy: regexp_case_equality_type(current, scope), falsey: falsey_acc, fully_narrowable: true }
|
|
3074
|
+
end
|
|
3075
|
+
|
|
2974
3076
|
target = when_condition_class(condition, scope)
|
|
2975
3077
|
return class_when_result(scope, current, target, falsey_acc) if target
|
|
2976
3078
|
|
|
@@ -3793,9 +3895,12 @@ module Rigor
|
|
|
3793
3895
|
# that edge, so a `$_` the earlier operand narrowed is forgotten when the operand may set it
|
|
3794
3896
|
# (`gets && log(items.each { gets })`), before the operand's own edges narrow it again.
|
|
3795
3897
|
# Issue #1429 — likewise a guard's narrowing of a global or constant the operand may rebind
|
|
3796
|
-
# (`$sep && reset_sep && $sep.length`).
|
|
3898
|
+
# (`$sep && reset_sep && $sep.length`). Issue #1963 — and an instance variable's narrowing an implicit-self
|
|
3899
|
+
# or `self.` call in the operand may write ({InterveningIvarReset.after_operand}): `@x.nil? && reset!` does
|
|
3900
|
+
# not leave `@x` `nil` when `reset!` assigns it.
|
|
3797
3901
|
def operand_edges(node, edge_scope)
|
|
3798
3902
|
edge_scope = LastLine.forget_if_set(edge_scope, node)
|
|
3903
|
+
edge_scope = InterveningIvarReset.after_operand(node, edge_scope)
|
|
3799
3904
|
if edge_scope.guard_narrowed? && GuardRebinding.may_rebind?(node, edge_scope)
|
|
3800
3905
|
edge_scope = edge_scope.forget_guard_narrowings
|
|
3801
3906
|
end
|
|
@@ -123,9 +123,14 @@ module Rigor
|
|
|
123
123
|
# all.
|
|
124
124
|
def answer_for_entry(scope, entry, method_name)
|
|
125
125
|
unless entry.external?
|
|
126
|
-
|
|
126
|
+
defines = source_defines?(scope, entry.name, method_name) || macro_named?(scope, entry.name, method_name)
|
|
127
|
+
return defines ? [:source, entry.name] : nil
|
|
127
128
|
end
|
|
128
129
|
|
|
130
|
+
# Issue #2094 — a project module with no `def` is held as an external; a macro of its body may name it.
|
|
131
|
+
named = entry.candidates.find { |candidate| macro_named?(scope, candidate, method_name) }
|
|
132
|
+
return [:source, named] if named
|
|
133
|
+
|
|
129
134
|
entry.candidates.each do |candidate|
|
|
130
135
|
definition = Rigor::Reflection.instance_method_definition(candidate, method_name, scope: scope)
|
|
131
136
|
return [private_definition?(definition) ? :private_rbs : :rbs, candidate, definition] if definition
|
|
@@ -137,6 +142,13 @@ module Rigor
|
|
|
137
142
|
end
|
|
138
143
|
private_class_method :answer_for_entry
|
|
139
144
|
|
|
145
|
+
# Issue #2094 — whether a class-body macro of `owner` was handed `method_name` as a literal
|
|
146
|
+
# (`dsl_method :xdyn`, `def_delegators :@target, :xdyn`). The indexer files every such name as an opaque
|
|
147
|
+
# envelope, since what the macro defines under it is unknown, so the module may define the name.
|
|
148
|
+
def macro_named?(scope, owner, method_name)
|
|
149
|
+
scope.parameter_envelopes_of(owner).key?([:instance, method_name.to_sym])
|
|
150
|
+
end
|
|
151
|
+
|
|
140
152
|
def private_definition?(definition)
|
|
141
153
|
definition.respond_to?(:accessibility) && definition.accessibility == :private
|
|
142
154
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "scope_indexer"
|
|
6
|
+
require_relative "project_seed_tables"
|
|
6
7
|
require_relative "fork_map"
|
|
7
8
|
require_relative "../source/node_walker"
|
|
8
9
|
|
|
@@ -238,18 +239,10 @@ module Rigor
|
|
|
238
239
|
base.with_discovery(base.discovery.with(**tables))
|
|
239
240
|
end
|
|
240
241
|
|
|
242
|
+
# Issue #1614 — the tables a `check` run seeds, from the one list {ProjectSeedTables} keeps for every path that
|
|
243
|
+
# runs no `check`, so an argument types here as it does in the file `check` analyses.
|
|
241
244
|
def discovery_seed_tables
|
|
242
|
-
|
|
243
|
-
def_index = ScopeIndexer.discovered_def_index_for_paths(@files)
|
|
244
|
-
tables = {}
|
|
245
|
-
tables[:discovered_classes] = classes unless classes.empty?
|
|
246
|
-
DISCOVERY_FIELD.each do |index_key, field|
|
|
247
|
-
table = def_index.fetch(index_key)
|
|
248
|
-
tables[field] = table unless table.empty? && !Scope::DiscoveryIndex.paired?(field)
|
|
249
|
-
end
|
|
250
|
-
# ADR-119 WD1 — the pairs travel together: a pair is dropped only when both halves are empty. A sibling whose
|
|
251
|
-
# member this collector does not seed completes to that member's empty value, which the base scope holds.
|
|
252
|
-
Scope::DiscoveryIndex.compact_pairs(tables.merge(def_index.fetch(:siblings)))
|
|
245
|
+
ProjectSeedTables.for_paths(@files)
|
|
253
246
|
rescue StandardError => e
|
|
254
247
|
# Discovery is best-effort; a malformed corner of the project must not crash the
|
|
255
248
|
# protection scan. Without discovery the collector simply resolves fewer call sites. A broken
|
|
@@ -257,21 +250,6 @@ module Rigor
|
|
|
257
250
|
e.is_a?(DeclarationWalk::ContractError) ? raise : {}
|
|
258
251
|
end
|
|
259
252
|
|
|
260
|
-
DISCOVERY_FIELD = {
|
|
261
|
-
def_nodes: :discovered_def_nodes,
|
|
262
|
-
singleton_def_nodes: :discovered_singleton_def_nodes,
|
|
263
|
-
def_sources: :discovered_def_sources,
|
|
264
|
-
superclasses: :discovered_superclasses,
|
|
265
|
-
header_nestings: :discovered_header_nestings,
|
|
266
|
-
includes: :discovered_includes, unpositioned_mixins: :unpositioned_mixins,
|
|
267
|
-
class_sources: :discovered_class_sources,
|
|
268
|
-
method_visibilities: :discovered_method_visibilities,
|
|
269
|
-
methods: :discovered_methods,
|
|
270
|
-
data_member_layouts: :data_member_layouts,
|
|
271
|
-
struct_member_layouts: :struct_member_layouts, deferred_ranges: :discovered_deferred_ranges
|
|
272
|
-
}.freeze
|
|
273
|
-
private_constant :DISCOVERY_FIELD
|
|
274
|
-
|
|
275
253
|
# Built once over the whole project and inherited by the fork workers via copy-on-write; a frozen Set
|
|
276
254
|
# for O(1) membership in the {#record_call} hot loop.
|
|
277
255
|
def user_method_names(discovery)
|
|
@@ -27,25 +27,57 @@ module Rigor
|
|
|
27
27
|
# alias earlier in the `pre_eval:` files binds `old`, the alias's entry copies that entry's `return_type` (and
|
|
28
28
|
# its `alias_of`); otherwise `alias_of` names `old`, a method the class had before the patch, and the
|
|
29
29
|
# dispatcher answers the alias with what that method answers. `alias_of` is nil for a `def`.
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
#
|
|
31
|
+
# Issue #1708 — `visibility` is `:public`, `:private` or `:protected`, as the patch file leaves the method: a
|
|
32
|
+
# `private def x`, a `def` after a bare `private` in the same body, a later `private :x`, and the instance copy
|
|
33
|
+
# a `module_function` makes are `:private` ({#callable?} reads it). An alias takes its old name's visibility,
|
|
34
|
+
# as Ruby's does; one whose old name no patch binds is `:public`.
|
|
35
|
+
#
|
|
36
|
+
# `farther` is set only on an entry {#lookup_through_ancestors} returns: the other definers of the name farther
|
|
37
|
+
# on the walk (entries, or {SOURCE_DEFINER} for a project-source `def`), which {#callable?} reads before it
|
|
38
|
+
# refuses the call.
|
|
39
|
+
Entry = Data.define(:class_name, :method_name, :kind, :source_path, :source_line, :return_type, :alias_of,
|
|
40
|
+
:visibility, :farther) do
|
|
41
|
+
def initialize(class_name:, method_name:, kind:, source_path:, source_line:, return_type: nil, alias_of: nil,
|
|
42
|
+
visibility: :public, farther: nil)
|
|
32
43
|
super
|
|
33
44
|
end
|
|
34
45
|
end
|
|
35
46
|
|
|
36
|
-
|
|
47
|
+
# Issue #1708 — one `include` or `prepend` a `pre_eval:` file writes (`Numeric.include(M)`, or `include M` in
|
|
48
|
+
# `class Numeric`): `owner` gains `module_name` as an ancestor, `kind` `:include` or `:prepend`. Names are as
|
|
49
|
+
# written, with no lexical resolution, as a patch's class name is.
|
|
50
|
+
Mixin = Data.define(:owner, :module_name, :kind)
|
|
51
|
+
|
|
52
|
+
attr_reader :by_key, :write_census, :mixins, :overrides
|
|
53
|
+
|
|
54
|
+
# Each owner's prepended and included modules, the most recent first.
|
|
55
|
+
def self.mixins_by_owner(mixins)
|
|
56
|
+
table = mixins.each_with_object({}) do |mixin, acc|
|
|
57
|
+
edges = (acc[mixin.owner] ||= { prepend: [], include: [] })
|
|
58
|
+
edges[mixin.kind].unshift(mixin.module_name)
|
|
59
|
+
end
|
|
60
|
+
table.each_value { |edges| edges.each_value(&:freeze) }.freeze
|
|
61
|
+
end
|
|
37
62
|
|
|
38
63
|
# @param entries — flat list of declarations observed during the pre-pass.
|
|
39
64
|
# Last-write-wins on `(class_name, method_name, kind)` duplicates, as at runtime (ADR-17); the
|
|
40
65
|
# `pre-eval.duplicate-declaration` diagnostic emission stays decoupled from registry behaviour.
|
|
41
66
|
# @param write_census — issue #1367: the `pre_eval:` files' {GlobalWriteCensus}, which the `global.*` write
|
|
42
67
|
# rules join with the project's, since a patch file is loaded ahead of the code it patches.
|
|
43
|
-
|
|
68
|
+
# @param mixins — issue #1708: the {Mixin} edges the `pre_eval:` files write, in the order they run.
|
|
69
|
+
# @param overrides — issue #1708: `[class_name, method_name, kind] => visibility` for a named visibility call
|
|
70
|
+
# (`public :kp` in `class Integer`) on a method the class inherits rather than binds, which Ruby copies onto
|
|
71
|
+
# the class with that visibility.
|
|
72
|
+
def initialize(entries: [], write_census: GlobalWriteCensus::EMPTY, mixins: [], overrides: {})
|
|
44
73
|
@by_key = entries.each_with_object({}) do |entry, acc|
|
|
45
74
|
key = [entry.class_name, entry.method_name, entry.kind]
|
|
46
75
|
acc[key] = entry
|
|
47
76
|
end.freeze
|
|
48
77
|
@write_census = write_census.frozen? ? write_census : write_census.dup.freeze
|
|
78
|
+
@mixins = mixins.dup.freeze
|
|
79
|
+
@mixins_of = ProjectPatchedMethods.mixins_by_owner(@mixins)
|
|
80
|
+
@overrides = overrides.dup.freeze
|
|
49
81
|
freeze
|
|
50
82
|
end
|
|
51
83
|
|
|
@@ -55,11 +87,143 @@ module Rigor
|
|
|
55
87
|
@by_key[[class_name, method_name, kind]]
|
|
56
88
|
end
|
|
57
89
|
|
|
90
|
+
# Issue #1708 — the entry a `kind` receiver of `class_name` reaches through its ancestors, searched in Ruby's
|
|
91
|
+
# order: a patch on `Object`, `Kernel`, `Numeric`, `Comparable` or `Enumerable` answers an `Integer`, `Array`
|
|
92
|
+
# or `String` receiver, and a patch on a nearer ancestor wins over one on a farther. A class object's chain
|
|
93
|
+
# is its singleton classes, then `Class`, `Module`, `Object`, `Kernel` and `BasicObject`, so `def zz` in
|
|
94
|
+
# `class Object` answers `Integer.zz` too. The ancestors are the class's RBS ones; a class RBS does not
|
|
95
|
+
# declare is searched as itself only, as {#lookup} searches it.
|
|
96
|
+
#
|
|
97
|
+
# It answers only which patch the chain holds. A method a nearer ancestor's RBS declares wins over the patch
|
|
98
|
+
# in Ruby, so the callers ask once that RBS has come back without the method. A method the project's own
|
|
99
|
+
# source defines on the way wins too: `source_defines` is called with each `owner, side` BEFORE that owner's
|
|
100
|
+
# patch is read, and when it answers true the walk stops with no entry, leaving the call to what types that
|
|
101
|
+
# method. That includes an owner holding both a patch and a source `def` (a `lib/` reopen loaded after the
|
|
102
|
+
# patch, or a module the project prepends to it), unlike {#lookup}, which answers the patch.
|
|
103
|
+
#
|
|
104
|
+
# A module the `pre_eval:` files include into or prepend to an owner on the way ({Mixin}) is searched where Ruby
|
|
105
|
+
# puts it: prepends ahead of the owner, includes after it, the most recent first, each with its own mixins. A
|
|
106
|
+
# named visibility call on an owner the walk passes before the body (`public :kp` in `class Integer`, an
|
|
107
|
+
# override) gives the entry it returns that visibility, and that owner for {#callable?}'s protected check.
|
|
108
|
+
# @return the entry, or `nil` when no ancestor's patch declares the method ahead of a source `def`.
|
|
109
|
+
def lookup_through_ancestors(class_name:, method_name:, kind:, rbs_loader:, source_defines: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
110
|
+
return nil if empty?
|
|
111
|
+
|
|
112
|
+
override = nil
|
|
113
|
+
found = nil
|
|
114
|
+
farther = []
|
|
115
|
+
ancestor_keys(class_name.to_s, kind, rbs_loader).each do |owner, side|
|
|
116
|
+
if source_defines&.call(owner, side)
|
|
117
|
+
return nil if found.nil?
|
|
118
|
+
|
|
119
|
+
farther << SOURCE_DEFINER
|
|
120
|
+
break
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
entry = @by_key[[owner, method_name, side]]
|
|
124
|
+
if found
|
|
125
|
+
farther << entry if entry
|
|
126
|
+
elsif entry
|
|
127
|
+
found = override ? entry.with(class_name: override.first, visibility: override.last) : entry
|
|
128
|
+
elsif (visibility = @overrides[[owner, method_name, side]])
|
|
129
|
+
override ||= [owner, visibility]
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
farther.empty? || found.nil? ? found : found.with(farther: farther.freeze)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A project-source `def` of the name farther on the walk than the entry it returns, in {Entry#farther}.
|
|
136
|
+
SOURCE_DEFINER = Object.new.freeze
|
|
137
|
+
|
|
138
|
+
# Issue #1708 — whether Ruby lets a call reach `entry`. A public method answers any call; a private one only a
|
|
139
|
+
# call with no receiver or with `self` as it (`explicit` is false for those); a protected one also a call from
|
|
140
|
+
# a method whose `self` is an instance of the entry's owner. `self_type` is the caller's `self` (`nil` at the
|
|
141
|
+
# top level, where it is `main`, an Object); a `self` this cannot name (the default) may be one, so a
|
|
142
|
+
# protected entry answers it.
|
|
143
|
+
#
|
|
144
|
+
# It never refuses a call some other definer of the name may answer: the walk's order is an approximation
|
|
145
|
+
# (a module two owners mix in is spliced at the nearer one, where Ruby keeps the first include and skips the
|
|
146
|
+
# second), so when {Entry#farther} holds a definer the call may reach (a project-source `def`, or an entry this
|
|
147
|
+
# would let it reach), it answers nil: decline, neither refused nor typed from `entry`.
|
|
148
|
+
# @return true (the call reaches `entry`), false (Ruby refuses it), or nil (declined).
|
|
149
|
+
def callable?(entry, explicit:, self_type: :unknown, rbs_loader: nil)
|
|
150
|
+
return true unless explicit
|
|
151
|
+
|
|
152
|
+
allowed =
|
|
153
|
+
case entry.visibility
|
|
154
|
+
when :private, :module_function then false
|
|
155
|
+
when :protected then self_kind_of?(self_type, entry.class_name, rbs_loader)
|
|
156
|
+
else true
|
|
157
|
+
end
|
|
158
|
+
return true if allowed
|
|
159
|
+
|
|
160
|
+
reachable = (entry.farther || []).any? do |other|
|
|
161
|
+
other.equal?(SOURCE_DEFINER) || callable?(other, explicit: true, self_type: self_type, rbs_loader: rbs_loader)
|
|
162
|
+
end
|
|
163
|
+
reachable ? nil : false # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
|
|
164
|
+
end
|
|
165
|
+
|
|
58
166
|
def empty?
|
|
59
167
|
@by_key.empty?
|
|
60
168
|
end
|
|
61
169
|
|
|
62
170
|
EMPTY = new.freeze
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
# Whether `self_type` may be an instance of `owner`: false only when the ancestors of a class RBS knows,
|
|
175
|
+
# with the patches' mixins, do not hold it.
|
|
176
|
+
def self_kind_of?(self_type, owner, rbs_loader)
|
|
177
|
+
name, kind =
|
|
178
|
+
if self_type.nil? then ["Object", :instance]
|
|
179
|
+
elsif self_type.respond_to?(:class_name) && self_type.class_name
|
|
180
|
+
[self_type.class_name.to_s, self_type.is_a?(Type::Singleton) ? :singleton : :instance]
|
|
181
|
+
end
|
|
182
|
+
return true if name.nil? || rbs_loader.nil?
|
|
183
|
+
|
|
184
|
+
keys = ancestor_keys(name, kind, rbs_loader)
|
|
185
|
+
keys == [[name, kind]] || keys.include?([owner, :instance])
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def ancestor_keys(class_name, kind, rbs_loader)
|
|
189
|
+
pairs =
|
|
190
|
+
if kind == :singleton
|
|
191
|
+
sides = rbs_loader ? rbs_loader.singleton_ancestors_for(class_name) : []
|
|
192
|
+
sides.empty? ? [[class_name, :singleton]] : sides.map { |side, owner| [owner, side] }
|
|
193
|
+
else
|
|
194
|
+
names = rbs_loader ? rbs_loader.ancestor_names_for(class_name) : []
|
|
195
|
+
(names.empty? ? [class_name] : names).map { |owner| [owner, :instance] }
|
|
196
|
+
end
|
|
197
|
+
@mixins_of.empty? ? pairs : with_mixins(pairs)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# The RBS ancestor pairs with each instance-side owner's `pre_eval:` mixins spliced in. A module already on
|
|
201
|
+
# the chain is not added again, as Ruby skips a module the ancestors already hold.
|
|
202
|
+
def with_mixins(pairs)
|
|
203
|
+
chain = pairs.filter_map { |owner, side| owner if side == :instance }.to_set
|
|
204
|
+
seen = Set.new
|
|
205
|
+
pairs.flat_map do |owner, side|
|
|
206
|
+
side == :instance ? expand_mixins(owner, chain, seen).map { |name| [name, :instance] } : [[owner, side]]
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# `owner` with the modules the patches prepend to it ahead and the ones they include after, each expanded the
|
|
211
|
+
# same way: the order Ruby's `ancestors` lists them in. An include of a module the RBS chain already holds
|
|
212
|
+
# changes nothing, as Ruby skips it (`Integer.include(Comparable)`); a prepend of one is spliced ahead all the
|
|
213
|
+
# same, as Ruby adds it again (`Integer.prepend(Comparable)`). `seen` holds the modules already spliced, so a
|
|
214
|
+
# module is spliced once, at the nearest owner that mixes it in, and an include cycle ends. Ruby skips
|
|
215
|
+
# `Integer.include(A)` run after `Numeric.include(A)`, leaving A after Numeric; the registry, which does not read
|
|
216
|
+
# the order the two edges ran in, splices A after Integer either way.
|
|
217
|
+
def expand_mixins(owner, chain, seen)
|
|
218
|
+
edges = @mixins_of[owner]
|
|
219
|
+
return [owner] if edges.nil?
|
|
220
|
+
|
|
221
|
+
ahead = edges[:prepend].flat_map { |name| seen.add?(name) ? expand_mixins(name, chain, seen) : [] }
|
|
222
|
+
after = edges[:include].flat_map do |name|
|
|
223
|
+
!chain.include?(name) && seen.add?(name) ? expand_mixins(name, chain, seen) : []
|
|
224
|
+
end
|
|
225
|
+
ahead + [owner] + after
|
|
226
|
+
end
|
|
63
227
|
end
|
|
64
228
|
end
|
|
65
229
|
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "project_patched_methods"
|
|
6
|
+
require_relative "../source/constant_path"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
module ProjectPatchedScanner
|
|
11
|
+
# Issue #1708 — the `include` / `prepend` edges a `pre_eval:` file writes, so a module it mixes into an
|
|
12
|
+
# ancestor (`Numeric.include(M)`) answers the receivers below that ancestor through
|
|
13
|
+
# {ProjectPatchedMethods#lookup_through_ancestors}. Ruby 4.0.5: after `Numeric.include(NumExt)`, `1.inc_m` and
|
|
14
|
+
# `1.5.inc_m` run.
|
|
15
|
+
module Mixins
|
|
16
|
+
# An `include` / `prepend` the walk met, with the lexical prefix {.resolve} resolves its module's written
|
|
17
|
+
# name in.
|
|
18
|
+
Pending = Data.define(:owner, :module_name, :kind, :prefix)
|
|
19
|
+
|
|
20
|
+
CALLS = %i[include prepend].freeze
|
|
21
|
+
private_constant :CALLS
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# The {Pending} edges `node` writes, or none: `Numeric.include(M)` on a constant, or `include M` /
|
|
26
|
+
# `self.include(M)` in a body. A top-level `include M` includes into `Object`, as Ruby's does. One in a
|
|
27
|
+
# `class << self` body is an `extend`, which this does not read; a module argument that is not a constant is
|
|
28
|
+
# skipped.
|
|
29
|
+
def pending(node, qualified_prefix, in_singleton_class)
|
|
30
|
+
return [] unless CALLS.include?(node.name) && node.arguments
|
|
31
|
+
|
|
32
|
+
owner = owner(node.receiver, qualified_prefix, in_singleton_class)
|
|
33
|
+
return [] if owner.nil?
|
|
34
|
+
|
|
35
|
+
node.arguments.arguments.filter_map do |arg|
|
|
36
|
+
name = Source::ConstantPath.qualified_name_or_nil(arg)
|
|
37
|
+
next if name.nil?
|
|
38
|
+
|
|
39
|
+
prefix = arg.is_a?(Prism::ConstantPathNode) && arg.parent.nil? ? [] : qualified_prefix
|
|
40
|
+
Pending.new(owner: owner, module_name: name, kind: node.name, prefix: prefix)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def owner(receiver, qualified_prefix, in_singleton_class)
|
|
45
|
+
case receiver
|
|
46
|
+
when nil, Prism::SelfNode
|
|
47
|
+
return nil if in_singleton_class
|
|
48
|
+
return "Object" if qualified_prefix.empty? && receiver.nil?
|
|
49
|
+
|
|
50
|
+
qualified_prefix.join("::") unless qualified_prefix.empty?
|
|
51
|
+
else
|
|
52
|
+
Source::ConstantPath.qualified_name_or_nil(receiver)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
private_class_method :owner
|
|
56
|
+
|
|
57
|
+
# The `include` / `prepend` call an idempotence guard runs only while its module is not yet an ancestor of its
|
|
58
|
+
# receiver, or nil. The guards are `X.include?(M)`, `X.ancestors.include?(M)` and `X < M`, written after the
|
|
59
|
+
# call as `unless` or as `if !`; the guard only skips a second include, so the call is read as a direct
|
|
60
|
+
# statement would be (#2109). The guard names the call's own receiver and module; any other condition, or a
|
|
61
|
+
# guard naming another receiver or module, leaves the call unread.
|
|
62
|
+
#
|
|
63
|
+
# `top_level` says the conditional is a direct statement of the file's top level, where a receiverless call
|
|
64
|
+
# includes into Object; see {guarded_call?}. The `CALLS` check is defensive: the scanner applies a call's mixin
|
|
65
|
+
# edge only when {Mixins.pending} reads it as include or prepend, so a call of another name changes nothing.
|
|
66
|
+
def guarded(node, top_level)
|
|
67
|
+
return nil unless node.is_a?(Prism::UnlessNode) || node.is_a?(Prism::IfNode)
|
|
68
|
+
|
|
69
|
+
call = guarded_body(node)
|
|
70
|
+
return nil unless call.is_a?(Prism::CallNode) && CALLS.include?(call.name) && guarded_call?(call, top_level)
|
|
71
|
+
|
|
72
|
+
guard = when_undefined(node)
|
|
73
|
+
call if guard && guards?(guard, call)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Whether `call` takes exactly one module, and is not receiverless at the file's top level. Such an include's
|
|
77
|
+
# guard would be `include?` on main, which Ruby does not define: the guard raises, so it is not read as an
|
|
78
|
+
# include into Object. A receiverless call in a class or module body names that body's own module.
|
|
79
|
+
def guarded_call?(call, top_level)
|
|
80
|
+
call.arguments&.arguments&.size == 1 && !(top_level && call.receiver.nil?)
|
|
81
|
+
end
|
|
82
|
+
private_class_method :guarded_call?
|
|
83
|
+
|
|
84
|
+
# The sole statement of an `unless` or `if` body, or nil when the conditional has an else branch or another
|
|
85
|
+
# body.
|
|
86
|
+
def guarded_body(node)
|
|
87
|
+
return nil if node.is_a?(Prism::UnlessNode) ? node.else_clause : node.subsequent
|
|
88
|
+
|
|
89
|
+
body = node.statements&.body
|
|
90
|
+
body.first if body&.size == 1
|
|
91
|
+
end
|
|
92
|
+
private_class_method :guarded_body
|
|
93
|
+
|
|
94
|
+
# The predicate the body runs under: `unless P` runs its body when P is false; `if !P` does too.
|
|
95
|
+
def when_undefined(node)
|
|
96
|
+
predicate = unwrap(node.predicate)
|
|
97
|
+
return predicate if node.is_a?(Prism::UnlessNode)
|
|
98
|
+
return nil unless predicate.is_a?(Prism::CallNode) && predicate.name == :! && predicate.receiver
|
|
99
|
+
|
|
100
|
+
unwrap(predicate.receiver)
|
|
101
|
+
end
|
|
102
|
+
private_class_method :when_undefined
|
|
103
|
+
|
|
104
|
+
def unwrap(node)
|
|
105
|
+
body = node.body.body if node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode)
|
|
106
|
+
body&.size == 1 ? body.first : node
|
|
107
|
+
end
|
|
108
|
+
private_class_method :unwrap
|
|
109
|
+
|
|
110
|
+
# Whether `guard` is `X.include?(M)`, `X.ancestors.include?(M)` or `X < M`, where `X` and `M` are the receiver
|
|
111
|
+
# and the argument of `call`; {guarded} passes only a call with exactly one argument.
|
|
112
|
+
def guards?(guard, call)
|
|
113
|
+
return false unless guard.is_a?(Prism::CallNode) && guard.arguments&.arguments&.size == 1
|
|
114
|
+
|
|
115
|
+
subject, named = guard_subject(guard)
|
|
116
|
+
named && same_constant?(subject, call.receiver) && same_constant?(named, call.arguments.arguments.first)
|
|
117
|
+
end
|
|
118
|
+
private_class_method :guards?
|
|
119
|
+
|
|
120
|
+
# The `[receiver, module]` a guard names, or nil.
|
|
121
|
+
def guard_subject(guard)
|
|
122
|
+
named = guard.arguments.arguments.first
|
|
123
|
+
case guard.name
|
|
124
|
+
when :<
|
|
125
|
+
[guard.receiver, named] if guard.receiver
|
|
126
|
+
when :include?
|
|
127
|
+
receiver = guard.receiver
|
|
128
|
+
if receiver.is_a?(Prism::CallNode) && receiver.name == :ancestors && receiver.arguments.nil?
|
|
129
|
+
[receiver.receiver, named]
|
|
130
|
+
else
|
|
131
|
+
[receiver, named]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
private_class_method :guard_subject
|
|
136
|
+
|
|
137
|
+
# Whether two receivers or modules name the same constant; two absent receivers (self) match.
|
|
138
|
+
def same_constant?(left, right)
|
|
139
|
+
return left.nil? && right.nil? if left.nil? || right.nil?
|
|
140
|
+
|
|
141
|
+
name = Source::ConstantPath.qualified_name_or_nil(left)
|
|
142
|
+
!name.nil? && name == Source::ConstantPath.qualified_name_or_nil(right)
|
|
143
|
+
end
|
|
144
|
+
private_class_method :same_constant?
|
|
145
|
+
|
|
146
|
+
# Each {Pending} edge's module name, resolved as Ruby resolves the constant from where it is written:
|
|
147
|
+
# `include NumExt` in `module Ext` names `Ext::NumExt` when a patch body opens that, else `NumExt`. Only the
|
|
148
|
+
# names the `pre_eval:` files open or mix into are candidates; one they do not name stays as written.
|
|
149
|
+
def resolve(pending, entries)
|
|
150
|
+
known = entries.to_set(&:class_name).merge(pending.map(&:owner))
|
|
151
|
+
pending.map do |item|
|
|
152
|
+
candidates = item.prefix.size.downto(1).map do |depth|
|
|
153
|
+
[*item.prefix.first(depth), item.module_name].join("::")
|
|
154
|
+
end
|
|
155
|
+
name = candidates.find { |candidate| known.include?(candidate) } || item.module_name
|
|
156
|
+
ProjectPatchedMethods::Mixin.new(owner: item.owner, module_name: name, kind: item.kind)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|