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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
require_relative "../type"
|
|
7
|
+
require_relative "captured_locals"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# Issues #1231 and #1839 — a rescue clause runs after the `begin` body raised from any point in it, and an `ensure`
|
|
12
|
+
# clause after the body, its `else` or a rescue clause raised, so each local or instance variable those wrote may
|
|
13
|
+
# hold any value one of their writes gave it, or the one it entered with. The engine does not track where the body
|
|
14
|
+
# raises; a clause enters with such a name rebound to `Dynamic[top]` instead ({.widen}).
|
|
15
|
+
#
|
|
16
|
+
# Not every name the body writes: one the clause mentions, whose binding the clause reads, and one the body
|
|
17
|
+
# writes more than once, which can hold a value neither the entry nor the body's exit binds. A name written once
|
|
18
|
+
# and never mentioned leaves the clause at its entry binding, which the join past the `begin` already pairs with
|
|
19
|
+
# the body's exit binding, the value of that one write (a write in a block or loop body included: the scope a
|
|
20
|
+
# block call or loop leaves joins every iteration's).
|
|
21
|
+
module ClauseWidening
|
|
22
|
+
WRITE_NODES = Set[
|
|
23
|
+
*CapturedLocals::LOCAL_WRITE_NODES,
|
|
24
|
+
Prism::InstanceVariableWriteNode,
|
|
25
|
+
Prism::InstanceVariableOperatorWriteNode,
|
|
26
|
+
Prism::InstanceVariableOrWriteNode,
|
|
27
|
+
Prism::InstanceVariableAndWriteNode,
|
|
28
|
+
Prism::InstanceVariableTargetNode
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
MENTION_NODES = Set[Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, *WRITE_NODES].freeze
|
|
32
|
+
|
|
33
|
+
SCOPE_BODY_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
34
|
+
|
|
35
|
+
# The names `sources` write: `names` all of them, `repeated` those written more than once, a nested block's
|
|
36
|
+
# included (it may write an outer local). A `def` or class body writes nothing here.
|
|
37
|
+
Writes = Data.define(:names, :repeated)
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# The {Writes} of `sources`, or nil for none.
|
|
42
|
+
def writes(sources)
|
|
43
|
+
counts = nil
|
|
44
|
+
sources.each { |source| counts = count_writes(source, counts) if source }
|
|
45
|
+
return nil if counts.nil?
|
|
46
|
+
|
|
47
|
+
Writes.new(names: counts.keys.to_set, repeated: counts.filter_map { |name, count| name if count > 1 }.to_set)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# `scope` with each name of `writes` that `clause` mentions, or that is repeated, rebound to `Dynamic[top]` where
|
|
51
|
+
# `scope` binds it, or nil when none changes. `clause` may be nil (an empty rescue clause).
|
|
52
|
+
def widen(scope, writes, clause)
|
|
53
|
+
return nil if writes.nil?
|
|
54
|
+
|
|
55
|
+
names = mentions(clause, writes.names, writes.repeated.dup)
|
|
56
|
+
untyped = Type::Combinator.untyped
|
|
57
|
+
widened = names.reduce(scope) do |acc, name|
|
|
58
|
+
ivar = name.start_with?("@")
|
|
59
|
+
current = ivar ? scope.ivar(name) : scope.local(name)
|
|
60
|
+
next acc if current.nil? || current == untyped
|
|
61
|
+
|
|
62
|
+
ivar ? acc.with_ivar(name, untyped).without_ivar_guard(name) : acc.with_local(name, untyped)
|
|
63
|
+
end
|
|
64
|
+
widened.equal?(scope) ? nil : widened
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# `counts` with each write under `node` counted.
|
|
68
|
+
def count_writes(node, counts)
|
|
69
|
+
return counts if SCOPE_BODY_NODES.any? { |klass| node.is_a?(klass) }
|
|
70
|
+
|
|
71
|
+
if WRITE_NODES.include?(node.class)
|
|
72
|
+
counts ||= Hash.new(0)
|
|
73
|
+
counts[node.name] += 1
|
|
74
|
+
end
|
|
75
|
+
node.rigor_each_child { |child| counts = count_writes(child, counts) }
|
|
76
|
+
counts
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# `found` with each name of `among` that `node` mentions added.
|
|
80
|
+
def mentions(node, among, found)
|
|
81
|
+
return found if node.nil? || SCOPE_BODY_NODES.any? { |klass| node.is_a?(klass) }
|
|
82
|
+
|
|
83
|
+
found << node.name if MENTION_NODES.include?(node.class) && among.include?(node.name)
|
|
84
|
+
node.rigor_each_child { |child| found = mentions(child, among, found) }
|
|
85
|
+
found
|
|
86
|
+
end
|
|
87
|
+
private_class_method :count_writes, :mentions
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../type"
|
|
5
|
+
require_relative "../source/node_walker"
|
|
6
|
+
require_relative "mutation_rejoin"
|
|
7
|
+
require_relative "receiver_alias"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# A loop hands its collection's own elements to its body (issue #2262), so a body that mutates one in place
|
|
12
|
+
# changes a shape that the collection's binding still carries one level down. This module is the two halves of
|
|
13
|
+
# dropping that shape: whether the body mutates an element, and the type with every shape dropped at every depth.
|
|
14
|
+
module DeepErasure
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# {MutationWidening.widen_tuple} / {MutationWidening.widen_hash_shape} applied at every depth: each `Tuple` /
|
|
18
|
+
# `HashShape` the type holds, as a union member or a type argument, drops to its nominal, so `[[1]]` reads
|
|
19
|
+
# `Array[Array[Integer]]`. A loop's element is the collection's own object, so a body that mutates it in place
|
|
20
|
+
# (`x << 2`) changes a shape one level down that the collection's binding still carries.
|
|
21
|
+
def widen(type)
|
|
22
|
+
case type
|
|
23
|
+
when Type::Tuple then widen_args(MutationWidening.widen_tuple(type))
|
|
24
|
+
when Type::HashShape then widen_args(MutationWidening.widen_hash_shape(type))
|
|
25
|
+
when Type::Nominal then widen_args(type)
|
|
26
|
+
when Type::Difference then widen(type.base)
|
|
27
|
+
when Type::Union then Type::Combinator.union(*type.members.map { |member| widen(member) })
|
|
28
|
+
else Type::Combinator.widen_value_pinned(type)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def widen_args(nominal)
|
|
33
|
+
return nominal unless nominal.is_a?(Type::Nominal) && !nominal.type_args.empty?
|
|
34
|
+
|
|
35
|
+
Type::Combinator.nominal_of(nominal.class_name, type_args: nominal.type_args.map { |arg| widen(arg) })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The parameter nodes whose names a block (or a `for` index) binds to the collection's elements.
|
|
39
|
+
ELEMENT_PARAMETER_NODES = [
|
|
40
|
+
Prism::RequiredParameterNode, Prism::OptionalParameterNode, Prism::RestParameterNode,
|
|
41
|
+
Prism::KeywordRestParameterNode, Prism::LocalVariableTargetNode
|
|
42
|
+
].freeze
|
|
43
|
+
ELEMENT_WRITE_NODES = [
|
|
44
|
+
Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode,
|
|
45
|
+
Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode,
|
|
46
|
+
Prism::IndexTargetNode, Prism::CallTargetNode
|
|
47
|
+
].freeze
|
|
48
|
+
private_constant :ELEMENT_PARAMETER_NODES, :ELEMENT_WRITE_NODES
|
|
49
|
+
|
|
50
|
+
# True when `body` mutates in place a value that a name in `names` (a loop's element) holds: a mutator
|
|
51
|
+
# (`MutationWidening::SHAPE_MUTATORS`) called on a read or an index chain rooted at it (`x << 1`,
|
|
52
|
+
# `x[0].push(1)`, `x[0] = 1`), or an operator write on one (`x[0] += 1`). Every name a block or a
|
|
53
|
+
# `for` inside the body binds counts as an element too (`x.each { |y| y << 1 }`, `for c in x; c << 1; end`),
|
|
54
|
+
# and so does `it` / `_1` wherever it is read, and an accumulator parameter (`inject([]) { |acc, e| acc << e }`):
|
|
55
|
+
# the rule is wider than "an element", which only loses precision. It does not follow an alias (`y = x`) or
|
|
56
|
+
# an element passed to a call (`add(x)`), which the straight-line widening does not either.
|
|
57
|
+
def element_mutation?(body, names)
|
|
58
|
+
return false if body.nil?
|
|
59
|
+
|
|
60
|
+
names |= nested_parameter_names(body)
|
|
61
|
+
Source::NodeWalker.each(body).any? { |node| element_mutation_site?(node, names) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The names `parameters` (a block's parameter list, or a `for` index) binds.
|
|
65
|
+
def parameter_names(parameters)
|
|
66
|
+
return [] if parameters.nil?
|
|
67
|
+
|
|
68
|
+
Source::NodeWalker.each(parameters).filter_map do |node|
|
|
69
|
+
node.name if ELEMENT_PARAMETER_NODES.include?(node.class)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def nested_parameter_names(body)
|
|
74
|
+
Source::NodeWalker.each(body).flat_map do |node|
|
|
75
|
+
case node
|
|
76
|
+
when Prism::BlockNode then parameter_names(node.parameters)
|
|
77
|
+
when Prism::ForNode then parameter_names(node.index)
|
|
78
|
+
else []
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def element_mutation_site?(node, names)
|
|
84
|
+
if node.is_a?(Prism::CallNode)
|
|
85
|
+
MutationWidening::SHAPE_MUTATORS.include?(node.name) && element_rooted?(node.receiver, names)
|
|
86
|
+
else
|
|
87
|
+
ELEMENT_WRITE_NODES.include?(node.class) && element_rooted?(node.receiver, names)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def element_rooted?(node, names)
|
|
92
|
+
node = node.receiver while node.is_a?(Prism::CallNode) && node.receiver
|
|
93
|
+
node = node.body.body.last while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode)
|
|
94
|
+
case node
|
|
95
|
+
when Prism::LocalVariableReadNode then names.include?(node.name) || /\A_[1-9]\z/.match?(node.name)
|
|
96
|
+
when Prism::ItLocalVariableReadNode then true
|
|
97
|
+
else false
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A block call hands each of its receiver's elements to the block's parameters, and a body that mutates one in
|
|
102
|
+
# place (`a.each { |x| x << 2 }`) changes the shape the receiver's binding still carries one level down
|
|
103
|
+
# (`[[1]]`). The receiver's variables drop their shapes at every depth ({widen}), as a `for` loop's
|
|
104
|
+
# value does ({StatementEvaluator#for_collection_after}).
|
|
105
|
+
def widen_element_aliases(receiver, body, names, scope)
|
|
106
|
+
return scope unless element_mutation?(body, names)
|
|
107
|
+
|
|
108
|
+
widen_receiver(receiver, scope)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def widen_receiver(receiver, scope)
|
|
112
|
+
ReceiverAlias.candidates(receiver).reduce(scope) { |acc, read| widen_read(read, acc) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# `a.each(&:strip!)`: a block argument naming a mutator mutates every element it is handed.
|
|
116
|
+
def symbol_proc_mutation?(block)
|
|
117
|
+
block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::SymbolNode) &&
|
|
118
|
+
MutationWidening::SHAPE_MUTATORS.include?(block.expression.unescaped.to_sym)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def widen_symbol_proc(call_node, scope) = widen_receiver(call_node.receiver, scope)
|
|
122
|
+
|
|
123
|
+
def widen_read(read, scope)
|
|
124
|
+
getter, builder = MutationWidening::ALIAS_ACCESSORS[read.class]
|
|
125
|
+
return scope if getter.nil?
|
|
126
|
+
|
|
127
|
+
name = ReceiverAlias.read_name(read)
|
|
128
|
+
current = scope.public_send(getter, name)
|
|
129
|
+
return scope if current.nil?
|
|
130
|
+
|
|
131
|
+
widened = widen(current)
|
|
132
|
+
return scope if widened == current
|
|
133
|
+
|
|
134
|
+
MutationRejoin.rebind(scope, builder, name, widened, pre_state: current, kind: getter)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "../type"
|
|
6
6
|
require_relative "../source/node_children"
|
|
7
7
|
require_relative "return_barrier"
|
|
8
|
+
require_relative "optimistic_origin"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Inference
|
|
@@ -21,10 +22,16 @@ module Rigor
|
|
|
21
22
|
#
|
|
22
23
|
# Extracted from `Rigor::SigGen::Generator#infer_return_type` so `LineTypeCollector` (`rigor annotate`'s
|
|
23
24
|
# def-line annotator) and the sig-generator share one source of truth.
|
|
25
|
+
#
|
|
26
|
+
# `keep_lookup_nil: true` is the reading a signature needs (#2197). The engine reads a lookup that can miss
|
|
27
|
+
# (`Hash#[]`, `Array#first`, the {OptimisticOrigin} family) as nil-free on purpose, so `if h[k]` stays
|
|
28
|
+
# silent, but a `-> V` written into `sig/` is a contract that every caller's `nil` check is dead code. Each
|
|
29
|
+
# returned expression whose value is such a lookup therefore contributes `nil` beside its type. Only the
|
|
30
|
+
# value positions a lookup's `nil` reaches the return through are followed ({#optimistic_value?}).
|
|
24
31
|
module DefReturnTyper
|
|
25
32
|
module_function
|
|
26
33
|
|
|
27
|
-
def call(def_node, scope_index)
|
|
34
|
+
def call(def_node, scope_index, keep_lookup_nil: false)
|
|
28
35
|
body = def_node.body
|
|
29
36
|
return nil if body.nil?
|
|
30
37
|
|
|
@@ -37,11 +44,152 @@ module Rigor
|
|
|
37
44
|
last_type = safe_type_of(inner_scope, last)
|
|
38
45
|
return nil if last_type.nil?
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
lookup_context = keep_lookup_nil ? LookupContext.new(scope_index, truthy_guards(body)) : nil
|
|
48
|
+
last_type = with_lookup_nil(last_type, last, inner_scope, lookup_context) if lookup_context
|
|
49
|
+
union_with_explicit_returns(body, last_type, scope_index, lookup_context: lookup_context)
|
|
41
50
|
rescue StandardError
|
|
42
51
|
nil
|
|
43
52
|
end
|
|
44
53
|
|
|
54
|
+
# What {#optimistic_value?} reads beside a node's scope: the engine's per-node scopes, and for each lookup
|
|
55
|
+
# under a guard the source of the guarding predicates ({#truthy_guards}).
|
|
56
|
+
LookupContext = Data.define(:scope_index, :guards)
|
|
57
|
+
|
|
58
|
+
def with_lookup_nil(type, node, scope, context = nil)
|
|
59
|
+
return type unless optimistic_value?(node, scope, context)
|
|
60
|
+
|
|
61
|
+
Type::Combinator.union(type, Type::Combinator.constant_of(nil))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Whether `node`'s value can be the `nil` an optimistic lookup's miss returns: the lookup itself (a call
|
|
65
|
+
# the dispatcher marked, or `recv&.m` over one), a local or ivar bound to one, or a branch or operand that
|
|
66
|
+
# hands its value on. `a && b` hands on either operand (a falsy `a`, a missed lookup's `nil` included, is
|
|
67
|
+
# the result); `a || b` hands on `b` only, since `a` is returned only when truthy, so `h[k] || 0` stays
|
|
68
|
+
# nil-free. `x ||= v` and `x &&= v` hand on `v` and, for `&&=`, the binding. A call ON the lookup
|
|
69
|
+
# (`h[k].to_s`) is a different value.
|
|
70
|
+
#
|
|
71
|
+
# Each child is read under the scope the engine recorded for it (`scope_index`), so a branch a guard narrows
|
|
72
|
+
# (`v ? v : "d"`, `return "z" unless v`) reads the binding the narrowing left, not the enclosing one.
|
|
73
|
+
def optimistic_value?(node, scope, context = nil)
|
|
74
|
+
scope = context&.scope_index&.[](node) || scope
|
|
75
|
+
case node
|
|
76
|
+
when Prism::CallNode then optimistic_call?(node, scope, context)
|
|
77
|
+
when Prism::LocalVariableReadNode then !scope.optimistic_local(node.name).nil?
|
|
78
|
+
when Prism::InstanceVariableReadNode then !scope.optimistic_ivar(node.name).nil?
|
|
79
|
+
when Prism::AndNode then [node.left, node.right].any? { optimistic_value?(it, scope, context) }
|
|
80
|
+
when Prism::OrNode then optimistic_value?(node.right, scope, context)
|
|
81
|
+
else optimistic_write?(node, scope, context) || optimistic_branch?(node, scope, context)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def optimistic_call?(node, scope, context)
|
|
86
|
+
if scope.optimistic_origins[node]
|
|
87
|
+
return false if guarded_lookup?(node, context) || non_empty_end?(node, scope)
|
|
88
|
+
|
|
89
|
+
return true
|
|
90
|
+
end
|
|
91
|
+
node.safe_navigation? && optimistic_value?(node.receiver, scope, context)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# `return h[k] if h[k]`, `h[k] ? h[k] : d`: the same lookup, read again under a predicate that just found it
|
|
95
|
+
# truthy. Only a lookup whose receiver and arguments are plain reads or literals counts, so the second read
|
|
96
|
+
# sees what the first did.
|
|
97
|
+
GUARDABLE_LOOKUPS = %i[[] first last dig].freeze
|
|
98
|
+
private_constant :GUARDABLE_LOOKUPS
|
|
99
|
+
|
|
100
|
+
def guarded_lookup?(node, context)
|
|
101
|
+
return false if context.nil? || !GUARDABLE_LOOKUPS.include?(node.name)
|
|
102
|
+
return false unless [node.receiver, *node.arguments&.arguments].all? { stable_operand?(it) }
|
|
103
|
+
|
|
104
|
+
context.guards.fetch(node, EMPTY_GUARDS).include?(node.slice)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
EMPTY_GUARDS = [].freeze
|
|
108
|
+
private_constant :EMPTY_GUARDS
|
|
109
|
+
|
|
110
|
+
def stable_operand?(node)
|
|
111
|
+
case node
|
|
112
|
+
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::SymbolNode, Prism::StringNode,
|
|
113
|
+
Prism::IntegerNode, Prism::SelfNode, Prism::ConstantReadNode then true
|
|
114
|
+
else false
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# `a.first` / `a.last` on a receiver the engine has narrowed to a non-empty array (`a.empty? ? d : a.first`)
|
|
119
|
+
# cannot miss.
|
|
120
|
+
def non_empty_end?(node, scope)
|
|
121
|
+
return false unless %i[first last].include?(node.name) && node.arguments.nil? && node.receiver
|
|
122
|
+
|
|
123
|
+
receiver = scope.type_of(node.receiver)
|
|
124
|
+
receiver.is_a?(Type::Difference) && receiver.removed == Type::Combinator.tuple_of
|
|
125
|
+
rescue StandardError
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def optimistic_write?(node, scope, context)
|
|
130
|
+
case node
|
|
131
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
|
|
132
|
+
Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode
|
|
133
|
+
optimistic_value?(node.value, scope, context)
|
|
134
|
+
when Prism::LocalVariableAndWriteNode
|
|
135
|
+
!scope.optimistic_local(node.name).nil? || optimistic_value?(node.value, scope, context)
|
|
136
|
+
when Prism::InstanceVariableAndWriteNode
|
|
137
|
+
!scope.optimistic_ivar(node.name).nil? || optimistic_value?(node.value, scope, context)
|
|
138
|
+
else false
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The value-carrying children of a statement list or a conditional: any of them can be the value returned.
|
|
143
|
+
def optimistic_branch?(node, scope, context)
|
|
144
|
+
value_branches(node).any? { |branch| !branch.nil? && optimistic_value?(branch, scope, context) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The lookup a truthy arm (the body of an `if` or ternary, the `else` of an `unless`) reads FIRST, mapped to
|
|
148
|
+
# the source of the predicate that just tested it, for {#guarded_lookup?}. Only the first expression, or the
|
|
149
|
+
# value of a first `return`, counts: anything run between the test and the re-read (`h.delete(k)`, a
|
|
150
|
+
# reassignment, a block, `yield`, another call that mutates the receiver) can make the re-read miss.
|
|
151
|
+
def truthy_guards(body)
|
|
152
|
+
guards = {}.compare_by_identity
|
|
153
|
+
collect_truthy_guards(body, guards)
|
|
154
|
+
guards
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def collect_truthy_guards(node, guards)
|
|
158
|
+
return unless node.is_a?(Prism::Node)
|
|
159
|
+
|
|
160
|
+
predicate, arm = truthy_arm(node)
|
|
161
|
+
first = first_value(arm)
|
|
162
|
+
guards[first] = [predicate.slice] if first
|
|
163
|
+
node.rigor_each_child { collect_truthy_guards(it, guards) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def first_value(arm)
|
|
167
|
+
return nil unless arm.is_a?(Prism::StatementsNode)
|
|
168
|
+
|
|
169
|
+
first = arm.body.first
|
|
170
|
+
args = first.is_a?(Prism::ReturnNode) ? first.arguments&.arguments : nil
|
|
171
|
+
args&.size == 1 ? args.first : first
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def truthy_arm(node)
|
|
175
|
+
case node
|
|
176
|
+
when Prism::IfNode then [node.predicate, node.statements]
|
|
177
|
+
when Prism::UnlessNode then [node.predicate, node.else_clause&.statements]
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def value_branches(node)
|
|
182
|
+
case node
|
|
183
|
+
when Prism::ParenthesesNode then [node.body && body_last_expression(node.body)]
|
|
184
|
+
when Prism::StatementsNode, Prism::BeginNode then [body_last_expression(node)]
|
|
185
|
+
when Prism::IfNode then [node.statements, node.subsequent]
|
|
186
|
+
when Prism::UnlessNode then [node.statements, node.else_clause]
|
|
187
|
+
when Prism::ElseNode, Prism::WhenNode, Prism::InNode then [node.statements]
|
|
188
|
+
when Prism::CaseNode, Prism::CaseMatchNode then [*node.conditions, node.else_clause]
|
|
189
|
+
else []
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
45
193
|
def body_last_expression(body)
|
|
46
194
|
case body
|
|
47
195
|
when Prism::StatementsNode then body.body.last
|
|
@@ -50,27 +198,27 @@ module Rigor
|
|
|
50
198
|
end
|
|
51
199
|
end
|
|
52
200
|
|
|
53
|
-
def union_with_explicit_returns(body, last_type, scope_index)
|
|
201
|
+
def union_with_explicit_returns(body, last_type, scope_index, lookup_context: nil)
|
|
54
202
|
return_types = []
|
|
55
|
-
collect_return_types(body, scope_index, return_types)
|
|
203
|
+
collect_return_types(body, scope_index, return_types, lookup_context: lookup_context)
|
|
56
204
|
return last_type if return_types.empty?
|
|
57
205
|
|
|
58
206
|
Type::Combinator.union(last_type, *return_types)
|
|
59
207
|
end
|
|
60
208
|
|
|
61
|
-
def collect_return_types(node, scope_index, out)
|
|
209
|
+
def collect_return_types(node, scope_index, out, lookup_context: nil)
|
|
62
210
|
return unless node.is_a?(Prism::Node)
|
|
63
211
|
return if ReturnBarrier.node?(node)
|
|
64
212
|
|
|
65
|
-
type_return_node(node, scope_index, out) if node.is_a?(Prism::ReturnNode)
|
|
213
|
+
type_return_node(node, scope_index, out, lookup_context: lookup_context) if node.is_a?(Prism::ReturnNode)
|
|
66
214
|
# A barrier call's receiver and arguments still run in the method; only its block is cut off.
|
|
67
215
|
barrier_block = node.block if node.is_a?(Prism::CallNode) && ReturnBarrier.block_call?(node)
|
|
68
216
|
node.rigor_each_child do |c|
|
|
69
|
-
collect_return_types(c, scope_index, out) unless c.equal?(barrier_block)
|
|
217
|
+
collect_return_types(c, scope_index, out, lookup_context: lookup_context) unless c.equal?(barrier_block)
|
|
70
218
|
end
|
|
71
219
|
end
|
|
72
220
|
|
|
73
|
-
def type_return_node(return_node, scope_index, out)
|
|
221
|
+
def type_return_node(return_node, scope_index, out, lookup_context: nil)
|
|
74
222
|
args = return_node.arguments&.arguments || []
|
|
75
223
|
if args.empty?
|
|
76
224
|
out << Type::Combinator.constant_of(nil)
|
|
@@ -84,7 +232,9 @@ module Rigor
|
|
|
84
232
|
return unless args.size == 1
|
|
85
233
|
|
|
86
234
|
type = safe_type_of(scope, args.first)
|
|
87
|
-
|
|
235
|
+
return if type.nil?
|
|
236
|
+
|
|
237
|
+
out << (lookup_context ? with_lookup_nil(type, args.first, scope, lookup_context) : type)
|
|
88
238
|
end
|
|
89
239
|
|
|
90
240
|
def safe_type_of(scope, node)
|
|
@@ -61,7 +61,7 @@ module Rigor
|
|
|
61
61
|
flavor = question == :arity ? :arity : :methods
|
|
62
62
|
chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, flavor)
|
|
63
63
|
|
|
64
|
-
reads_visibility = VISIBILITY_QUESTIONS.include?(question)
|
|
64
|
+
reads_visibility = VISIBILITY_QUESTIONS.include?(question) ? question : nil
|
|
65
65
|
hits = candidates(scope, chain, method_name, from, answer_in, reads_visibility)
|
|
66
66
|
return UNKNOWN if hits.nil?
|
|
67
67
|
|
|
@@ -159,23 +159,39 @@ module Rigor
|
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
# The implicit tail after a chain's last project superclass: `Object` (with `Kernel` and `BasicObject`) on the
|
|
162
|
-
# instance side, `#<Class:Object>` through `Class`, `Module` and `Kernel` on the singleton side.
|
|
162
|
+
# instance side, `#<Class:Object>` through `Class`, `Module` and `Kernel` on the singleton side. It may answer
|
|
163
|
+
# where RBS declares the name there, where the last class's superclass is not a constant the tables can follow
|
|
164
|
+
# (#1624, {Scope::ResolutionChain#opaque_tail?}: `class C < Struct.new(:a)`, `C = Class.new(Base)`), and where
|
|
165
|
+
# the project itself reopens a tail class with the name (`class Object; def self.x`), which is no entry on the
|
|
166
|
+
# chain because the class names no superclass.
|
|
163
167
|
def implicit_object_answers?(scope, chain, method_name)
|
|
164
|
-
!external_lacks?(scope, ["Object"], chain.side, method_name)
|
|
168
|
+
!external_lacks?(scope, ["Object"], chain.side, method_name) || chain.opaque_tail?(scope) ||
|
|
169
|
+
project_tail_defines?(scope, chain.side, method_name)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The implicit tail's classes and the side each is read on, per chain side: a class object reaches the instance
|
|
173
|
+
# methods of `Class`, `Module`, `Object`, `Kernel` and `BasicObject` and the singleton methods of `Object` and
|
|
174
|
+
# `BasicObject`.
|
|
175
|
+
CLASS_OBJECT_TAIL = %w[Class Module Object Kernel BasicObject].product([:instance]) +
|
|
176
|
+
%w[Object BasicObject].product([:singleton])
|
|
177
|
+
IMPLICIT_TAIL = Ractor.make_shareable({ instance: %w[Object Kernel BasicObject].product([:instance]),
|
|
178
|
+
singleton: CLASS_OBJECT_TAIL })
|
|
179
|
+
private_constant :CLASS_OBJECT_TAIL, :IMPLICIT_TAIL
|
|
180
|
+
|
|
181
|
+
# Whether a project reopening of a tail class records the name: a `def`, whose read files the method edge, or
|
|
182
|
+
# an `attr_*` / `define_method` the existence table holds.
|
|
183
|
+
def project_tail_defines?(scope, side, method_name)
|
|
184
|
+
IMPLICIT_TAIL.fetch(side).any? do |name, kind|
|
|
185
|
+
(kind == :singleton ? scope.singleton_def_for(name, method_name) : scope.user_def_for(name, method_name)) ||
|
|
186
|
+
scope.discovered_method?(name, method_name, kind)
|
|
187
|
+
end
|
|
165
188
|
end
|
|
166
189
|
|
|
167
190
|
# True only for an ancestor RBS knows whose declaration lacks the name, on the side the entry stands for: an
|
|
168
191
|
# external superclass on a singleton chain (`:singleton`, its whole tail) is asked for a singleton method, an
|
|
169
192
|
# extended module (`:instance`) and every instance-side entry for an instance method.
|
|
170
193
|
def external_lacks?(scope, candidates, side, method_name)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
|
|
174
|
-
return false if known.nil?
|
|
175
|
-
|
|
176
|
-
Rigor::Reflection.singleton_method_definition(known, method_name, scope: scope).nil?
|
|
177
|
-
rescue StandardError
|
|
178
|
-
false
|
|
194
|
+
Scope::ResolutionChain::Relevance.external_lacks_on?(scope, candidates, side, method_name)
|
|
179
195
|
end
|
|
180
196
|
|
|
181
197
|
# What a candidate set says, for {Scope::ResolutionChain#settle} to compare across worlds.
|
|
@@ -198,14 +214,27 @@ module Rigor
|
|
|
198
214
|
# that reads the visibility (`reads_visibility`): a slot is contested there for a visibility call the walk
|
|
199
215
|
# cannot place (an uncertain toggle, `private(*names)`, `private def self.x`), which leaves the definer and its
|
|
200
216
|
# `def` certain; a possible `def` or `attr_*` call contests its def-node slot or its name's `possible` side too.
|
|
217
|
+
#
|
|
218
|
+
# `reads_visibility` is the question when it reads one. The `:visibility` question does not read a contested
|
|
219
|
+
# instance def-node slot (#1989): it answers from the visibility slot and the name's existence, not from the
|
|
220
|
+
# `def` body, and a possible `def` that contests the slot also makes its name `possible`. The slot is contested
|
|
221
|
+
# alone where a later `attr_*` replaced the `def`: the attr writes the name's visibility slot, so the reader's
|
|
222
|
+
# visibility is certain. A `define_method` or alias that replaced it contests the visibility slot as well, which
|
|
223
|
+
# this question does read.
|
|
224
|
+
#
|
|
225
|
+
# Issue #2134 — the contest is a read of the slot, so a question that reads it files `visibility:<owner>#<name>`:
|
|
226
|
+
# an answer block may answer from RBS without reading the slot itself (`nearest_ancestor_method_def`).
|
|
201
227
|
def possible?(scope, hit, method_name, reads_visibility)
|
|
202
228
|
discovery = scope.discovery
|
|
203
229
|
owner = hit.owner
|
|
204
230
|
kind = hit.side == :singleton ? :singleton : :instance
|
|
205
231
|
nodes = kind == :singleton ? :discovered_singleton_def_nodes : :discovered_def_nodes
|
|
232
|
+
Analysis::DependencyRecorder.read_visibility(owner, method_name) if reads_visibility
|
|
206
233
|
discovery.possible_method?(owner, method_name, kind) ||
|
|
207
|
-
|
|
208
|
-
|
|
234
|
+
(!(reads_visibility == :visibility && kind == :instance) &&
|
|
235
|
+
discovery.contested?(nodes, [owner, method_name.to_sym])) ||
|
|
236
|
+
(reads_visibility && discovery.contested?(:discovered_method_visibilities,
|
|
237
|
+
[owner, visibility_slot(method_name, kind)])) ||
|
|
209
238
|
discovery.contested?(:discovered_parameter_envelopes, [owner, [kind, method_name.to_sym]])
|
|
210
239
|
end
|
|
211
240
|
|
|
@@ -235,20 +264,71 @@ module Rigor
|
|
|
235
264
|
|
|
236
265
|
# The first project entry that records the name (a discovered method, an instance `def` the def table
|
|
237
266
|
# holds, or a visibility change such as `private :foo` recorded for it), with its visibility (`:public` where
|
|
238
|
-
# none was stated).
|
|
267
|
+
# none was stated). Issue #1984 — a singleton-side entry reads its class's singleton slot
|
|
268
|
+
# (`private_class_method :new` records `new` there with no `def`), never the instance one.
|
|
239
269
|
def visibility_answer(scope, method_name)
|
|
240
270
|
lambda do |chain, from|
|
|
241
271
|
first_hit(chain, scope, from) do |entry|
|
|
242
272
|
kind = entry.side == :singleton ? :singleton : :instance
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
273
|
+
recorded = side_visibility(scope, entry.name, method_name, kind)
|
|
274
|
+
next unless scope.discovered_method?(entry.name, method_name, kind) || recorded ||
|
|
275
|
+
(kind == :instance && scope.user_def_for(entry.name, method_name))
|
|
276
|
+
|
|
277
|
+
recorded || :public
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def side_visibility(scope, owner, method_name, kind)
|
|
283
|
+
if kind == :singleton
|
|
284
|
+
scope.discovered_method_visibility(owner, method_name, singleton: true) ||
|
|
285
|
+
self_extended_copy_visibility(scope, owner, method_name)
|
|
286
|
+
else
|
|
287
|
+
scope.discovered_method_visibility(owner, method_name)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
246
290
|
|
|
247
|
-
|
|
291
|
+
# Issue #2059 — a module that `extend`s itself reaches its own instance method from the singleton side, and the
|
|
292
|
+
# extends fold copies that receiverless `def` onto the module's singleton entry, where no singleton slot records
|
|
293
|
+
# it: the instance slot's visibility is what Ruby applies (`module ES; extend self; private def x` makes `ES.x`
|
|
294
|
+
# private). A module function's singleton copy is public whatever its instance copy is, so a `module_function`
|
|
295
|
+
# row for the name in any file answers nil, as does a contested instance slot.
|
|
296
|
+
def self_extended_copy_visibility(scope, owner, method_name)
|
|
297
|
+
node = scope.singleton_def_for(owner, method_name)
|
|
298
|
+
return nil if node.nil? || !node.receiver.nil?
|
|
299
|
+
return nil unless scope.discovered_extends[owner.to_s]&.include?(owner.to_s)
|
|
300
|
+
return nil if scope.discovery.contested?(:discovered_method_visibilities, [owner.to_s, method_name.to_sym])
|
|
301
|
+
return nil if module_function_row?(scope, owner, method_name)
|
|
302
|
+
|
|
303
|
+
scope.discovered_method_visibility(owner, method_name)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Whether any file's deferred ranges carry a `module_function` row (`:both` or `:singleton`) for the name on
|
|
307
|
+
# `owner` (`ModuleFunctionState`).
|
|
308
|
+
#
|
|
309
|
+
# Issue #2161 — the answer reads every file's rows, so it records `visibility:<owner>#<name>` whichever way it
|
|
310
|
+
# answers: `ScopeIndexer.visibility_entries` carries each file's rows of this shape, so a file that starts or
|
|
311
|
+
# stops writing one fires the key. A swap from `private :x` to `module_function :x` in a reopening leaves the
|
|
312
|
+
# file's visibility table and declared classes as they were, and fires nothing else. The caller's own
|
|
313
|
+
# visibility reads record the same key today; this one keeps the reader's edge where it reads.
|
|
314
|
+
#
|
|
315
|
+
# Issue #2209 — a row named {ModuleFunctionState::ANY_ROW_NAME} (a `module_function` spelling whose names the
|
|
316
|
+
# prescan cannot read) stands for every name of its owner, so it counts too, and its key is read as well
|
|
317
|
+
# ({ModuleFunctionState.row_names}).
|
|
318
|
+
def module_function_row?(scope, owner, method_name)
|
|
319
|
+
names = ModuleFunctionState.row_names(method_name).each { Analysis::DependencyRecorder.read_visibility(owner, it) }
|
|
320
|
+
scope.discovered_deferred_ranges.each_value.any? do |rows|
|
|
321
|
+
rows.any? do |(_start, _finish, row_name, kind, row_owner)|
|
|
322
|
+
names.include?(row_name) && row_owner == owner.to_s && kind != :instance
|
|
248
323
|
end
|
|
249
324
|
end
|
|
250
325
|
end
|
|
251
326
|
|
|
327
|
+
# The key of a name's slot in the visibility table on `kind`'s side (`ScopeIndexer#visibility_slot`).
|
|
328
|
+
def visibility_slot(method_name, kind)
|
|
329
|
+
kind == :singleton ? [:singleton, method_name.to_sym] : method_name.to_sym
|
|
330
|
+
end
|
|
331
|
+
|
|
252
332
|
def first_hit(chain, scope, from)
|
|
253
333
|
index = from - 1
|
|
254
334
|
found = nil
|
|
@@ -265,9 +345,10 @@ module Rigor
|
|
|
265
345
|
end
|
|
266
346
|
|
|
267
347
|
private_class_method :override_answer, :retro_position, :candidates, :past_fold_copies, :decided,
|
|
268
|
-
:external_may_answer?, :implicit_object_answers?, :external_lacks?,
|
|
348
|
+
:external_may_answer?, :implicit_object_answers?, :project_tail_defines?, :external_lacks?,
|
|
269
349
|
:outcomes, :collapse, :possible?,
|
|
270
|
-
:default_answer, :definer_answer, :visibility_answer, :
|
|
350
|
+
:default_answer, :definer_answer, :visibility_answer, :side_visibility,
|
|
351
|
+
:self_extended_copy_visibility, :module_function_row?, :visibility_slot, :first_hit
|
|
271
352
|
end
|
|
272
353
|
end
|
|
273
354
|
end
|
|
@@ -53,8 +53,8 @@ module Rigor
|
|
|
53
53
|
# made a class guard on `$!` itself narrow it; #1447 narrows this decline to the guards that still do not.
|
|
54
54
|
#
|
|
55
55
|
# `$@` calls the exception's `backtrace`, which is an `Array[String]` for a raised exception. It is left unbound
|
|
56
|
-
# in a program that defines a method named `backtrace` anywhere, which may return anything,
|
|
57
|
-
#
|
|
56
|
+
# in a program that defines a method named `backtrace` anywhere, which may return anything, in a clause whose
|
|
57
|
+
# body calls `set_backtrace`, which may set it to nil, and in a clause whose body never reads it.
|
|
58
58
|
#
|
|
59
59
|
# `$?` is unbound because the exception may have been raised while a subprocess waited: a backtick, `%x` or
|
|
60
60
|
# `system` sets `$?` to nil before it runs the child, so a `Timeout::Error`, an `Interrupt` or a `Thread#raise`
|
|
@@ -65,6 +65,10 @@ module Rigor
|
|
|
65
65
|
return entered if guarded?(body, reference)
|
|
66
66
|
|
|
67
67
|
entered = entered.with_global(ERROR_INFO, rescued_type(exception_type, scope))
|
|
68
|
+
# Issue #1967 — a clause that never reads `$@` leaves it unbound without asking about `backtrace`: the answer
|
|
69
|
+
# could not reach a diagnostic, and asking records a `defines:backtrace` edge that re-checked every `rescue`
|
|
70
|
+
# in the program whenever such a def came or went.
|
|
71
|
+
return entered unless backtrace_read?(body)
|
|
68
72
|
return entered if calls_set_backtrace?(body) || BlockCallTiming.project_defines_anywhere?(:backtrace, scope)
|
|
69
73
|
|
|
70
74
|
entered.with_global(BACKTRACE, backtrace_type)
|
|
@@ -114,7 +118,17 @@ module Rigor
|
|
|
114
118
|
node.rigor_each_child { |child| found ||= calls_set_backtrace?(child) }
|
|
115
119
|
found
|
|
116
120
|
end
|
|
117
|
-
|
|
121
|
+
|
|
122
|
+
# True when `node` reads `$@` anywhere in it, a block included. An empty clause (nil) reads nothing.
|
|
123
|
+
def backtrace_read?(node)
|
|
124
|
+
return false unless node.is_a?(Prism::Node)
|
|
125
|
+
return node.name == BACKTRACE if node.is_a?(Prism::GlobalVariableReadNode)
|
|
126
|
+
|
|
127
|
+
found = false
|
|
128
|
+
node.rigor_each_child { |child| found ||= backtrace_read?(child) }
|
|
129
|
+
found
|
|
130
|
+
end
|
|
131
|
+
private_class_method :guard?, :error_info_read?, :calls_set_backtrace?, :backtrace_read?
|
|
118
132
|
|
|
119
133
|
# True when `node` reads `$!`, `$@` or `$?` anywhere in it: the gate on typing a rescue modifier's fallback
|
|
120
134
|
# under {.modifier_entry}, which most fallbacks (`rescue nil`) never need.
|