rigortype 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +56 -8
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +50 -35
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +16 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
require_relative "../source/parameter_envelope"
|
|
7
|
+
require_relative "../type"
|
|
8
|
+
require_relative "guard_rebinding"
|
|
9
|
+
require_relative "project_method_ownership"
|
|
10
|
+
require_relative "refined_dispatch/own_method"
|
|
11
|
+
|
|
12
|
+
module Rigor
|
|
13
|
+
module Inference
|
|
14
|
+
# Issue #1596 — the instance variables an implicit-self or `self.` statement call may write, when the analysis
|
|
15
|
+
# can prove that set. The intervening-call reset (`StatementEvaluator#invalidate_ivars_for_intervening_call`)
|
|
16
|
+
# and the class-guard restore ({GuardRebinding}) both assume such a call may write any instance variable and
|
|
17
|
+
# run any code; with a proven set they widen only the instance variables in it, so `return unless
|
|
18
|
+
# @io.is_a?(StringIO); log("dumping"); @io.string` keeps `@io` a `StringIO` when `log` writes no instance
|
|
19
|
+
# variable.
|
|
20
|
+
#
|
|
21
|
+
# {.of} answers a frozen `Set` of names, or nil when it cannot prove one. It proves one only for an instance
|
|
22
|
+
# method's `self` (a project class, not a module, whose instance chain the resolution chain settles) and a call
|
|
23
|
+
# with no block argument (`&blk`, `...`), and only when every method the call can reach is proven:
|
|
24
|
+
#
|
|
25
|
+
# - A method the project defines on the class's chain must resolve to one `def` (not `define_method`, an
|
|
26
|
+
# `attr_*` writer or an alias), found ahead of every external ancestor, with every project subclass resolving
|
|
27
|
+
# the name to the same definer, and no `refine` block defining the name. Its parameters' default values and
|
|
28
|
+
# its body are read. A recursive or mutually recursive call adds nothing the first read did not.
|
|
29
|
+
# - Any other method on `self` must be one {GuardRebinding} counts as running no foreign code (`warn`, `puts`,
|
|
30
|
+
# `format`), and not a code-running name (`send`, `instance_variable_set`, `instance_eval`, …).
|
|
31
|
+
# - A call on another receiver counts only on a literal, a global, a constant naming a core or standard-library
|
|
32
|
+
# class, or an instance variable read as its class-ivar seed, and only when {GuardRebinding} counts its method
|
|
33
|
+
# as running no foreign code there. A local, a parameter or a call result is a receiver the read cannot type.
|
|
34
|
+
#
|
|
35
|
+
# Read code that writes an instance variable adds its name (an `instance_variable_set` call is a code-running
|
|
36
|
+
# name, so it proves nothing). Code that writes a global or a constant, yields, calls `super`, defines a method
|
|
37
|
+
# or opens a singleton class, an operator write on a local, and a `for` loop prove nothing. A literal block at
|
|
38
|
+
# the call site or in a read body is read with the code around it. The accepted gap is {GuardRebinding}'s:
|
|
39
|
+
# an implicit conversion a core method runs (`warn obj` runs `obj.to_s`) is read as the core method alone, and
|
|
40
|
+
# so is a singleton method an `extend` or `def obj.m` gives one object at run time.
|
|
41
|
+
module SelfCallIvarWrites
|
|
42
|
+
# The most `def` bodies one proof reads before it gives up.
|
|
43
|
+
DEF_BUDGET = 32
|
|
44
|
+
# Code the proof does not read past: it rebinds a global or constant, runs code the body does not show, or
|
|
45
|
+
# changes which method a later call dispatches to.
|
|
46
|
+
UNPROVABLE_NODES = Set[
|
|
47
|
+
Prism::GlobalVariableWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
|
|
48
|
+
Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableTargetNode,
|
|
49
|
+
Prism::ConstantWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode,
|
|
50
|
+
Prism::ConstantOperatorWriteNode, Prism::ConstantTargetNode, Prism::ConstantPathWriteNode,
|
|
51
|
+
Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode, Prism::ConstantPathOperatorWriteNode,
|
|
52
|
+
Prism::ConstantPathTargetNode,
|
|
53
|
+
Prism::YieldNode, Prism::SuperNode, Prism::ForwardingSuperNode, Prism::ForwardingArgumentsNode,
|
|
54
|
+
Prism::BlockArgumentNode, Prism::DefNode, Prism::SingletonClassNode, Prism::ForNode,
|
|
55
|
+
Prism::LocalVariableOperatorWriteNode, Prism::ClassVariableOperatorWriteNode, Prism::CallOperatorWriteNode,
|
|
56
|
+
Prism::CallOrWriteNode, Prism::CallAndWriteNode, Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode,
|
|
57
|
+
Prism::IndexAndWriteNode
|
|
58
|
+
].freeze
|
|
59
|
+
# Literal receivers, by the core class their value is an instance of.
|
|
60
|
+
LITERAL_CLASSES = {
|
|
61
|
+
Prism::StringNode => "String", Prism::InterpolatedStringNode => "String", Prism::XStringNode => "String",
|
|
62
|
+
Prism::SymbolNode => "Symbol", Prism::InterpolatedSymbolNode => "Symbol", Prism::IntegerNode => "Integer",
|
|
63
|
+
Prism::FloatNode => "Float", Prism::ArrayNode => "Array", Prism::HashNode => "Hash",
|
|
64
|
+
Prism::RangeNode => "Range", Prism::RegularExpressionNode => "Regexp",
|
|
65
|
+
Prism::InterpolatedRegularExpressionNode => "Regexp", Prism::NilNode => "NilClass",
|
|
66
|
+
Prism::TrueNode => "TrueClass", Prism::FalseNode => "FalseClass"
|
|
67
|
+
}.freeze
|
|
68
|
+
# The methods `Kernel` or `Object` own that a read accepts on `self` when the project does not define them. Any
|
|
69
|
+
# other (`display`, `inspect`, `==`, `hash`, `extend`, `method`, `binding`, `instance_variables`) proves nothing.
|
|
70
|
+
# These may still convert a value they are given, which runs that value's methods: {PROTOCOL_NAMES} answers for
|
|
71
|
+
# a value that is `self`.
|
|
72
|
+
SELF_SAFE_NAMES = Set[
|
|
73
|
+
:warn, :puts, :print, :p, :pp, :format, :sprintf, :printf, :raise, :fail, :loop, :catch, :throw, :rand,
|
|
74
|
+
:sleep, :Integer, :Float, :String, :Array, :Hash, :Rational, :Complex, :block_given?, :frozen?, :nil?,
|
|
75
|
+
:is_a?, :kind_of?, :instance_of?, :object_id, :class, :__method__, :caller, :abort, :exit
|
|
76
|
+
].freeze
|
|
77
|
+
# Issue #1596 — the methods core code calls on a value without the program spelling them: conversions,
|
|
78
|
+
# comparisons, hashing, iteration, `respond_to?` and its hook, `exception` for `raise`, `divmod` for `sleep`.
|
|
79
|
+
# A value aliasing `self` (`@me = self`, `me = self`, a constant holding the instance) handed to a core
|
|
80
|
+
# method runs `self`'s own definition of one of these, which the read never sees, so a class defining any
|
|
81
|
+
# proves nothing.
|
|
82
|
+
PROTOCOL_NAMES = Set[
|
|
83
|
+
:to_s, :inspect, :hash, :eql?, :==, :===, :<=>, :to_a, :to_ary, :to_str, :to_hash, :to_int, :to_i, :to_f,
|
|
84
|
+
:to_r, :to_c, :to_proc, :to_path, :to_sym, :to_io, :coerce, :exception, :divmod, :each, :respond_to?,
|
|
85
|
+
:respond_to_missing?, :method_missing, :initialize_copy, :marshal_dump, :pretty_print
|
|
86
|
+
].freeze
|
|
87
|
+
private_constant :UNPROVABLE_NODES, :LITERAL_CLASSES, :SELF_SAFE_NAMES, :PROTOCOL_NAMES
|
|
88
|
+
|
|
89
|
+
# Raised inside a proof to abandon it.
|
|
90
|
+
class Unprovable < StandardError
|
|
91
|
+
end
|
|
92
|
+
private_constant :Unprovable
|
|
93
|
+
|
|
94
|
+
module_function
|
|
95
|
+
|
|
96
|
+
# The names of the instance variables `call_node`, an implicit-self or `self.` call, may write, as a frozen
|
|
97
|
+
# `Set`, or nil when the analysis cannot prove the set. `scope` is the scope the call's operands were typed in.
|
|
98
|
+
def of(call_node, scope)
|
|
99
|
+
return nil unless call_node.is_a?(Prism::CallNode)
|
|
100
|
+
return nil unless call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
|
|
101
|
+
|
|
102
|
+
self_type = scope.self_type
|
|
103
|
+
return nil unless self_type.is_a?(Type::Nominal)
|
|
104
|
+
|
|
105
|
+
proof = Proof.new(self_type, scope)
|
|
106
|
+
proof.no_self_value(call_node.arguments)
|
|
107
|
+
proof.no_protocol_override
|
|
108
|
+
proof.self_call(call_node)
|
|
109
|
+
proof.read(call_node.block)
|
|
110
|
+
proof.written.freeze
|
|
111
|
+
rescue StandardError # an `Unprovable`, or a lookup that raised: either proves nothing
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# One proof's state: the instance variables read code writes, and the `def` bodies already read.
|
|
116
|
+
class Proof
|
|
117
|
+
attr_reader :written
|
|
118
|
+
|
|
119
|
+
def initialize(self_type, scope)
|
|
120
|
+
@self_type = self_type
|
|
121
|
+
@class_name = self_type.class_name
|
|
122
|
+
@scope = scope
|
|
123
|
+
@written = Set.new
|
|
124
|
+
@visited = {}.compare_by_identity
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Reads the method an implicit-self or `self.` call to `call_node`'s name runs.
|
|
128
|
+
def self_call(call_node)
|
|
129
|
+
name = call_node.name
|
|
130
|
+
unprovable! if GuardRebinding.code_running_name?(name) || refined?(name)
|
|
131
|
+
|
|
132
|
+
if ProjectMethodOwnership.defines?(@class_name, name, :instance, @scope)
|
|
133
|
+
read_def(proven_def(name))
|
|
134
|
+
elsif !SELF_SAFE_NAMES.include?(name) || GuardRebinding.type_method_foreign?(@self_type, name, @scope)
|
|
135
|
+
unprovable!
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Issue #1596 — gives up when `self`'s class, a project subclass, or a project ancestor or module on either's
|
|
140
|
+
# chain defines a {PROTOCOL_NAMES} method (a `def`, `define_method`, `attr_*` or alias), rewrites its methods
|
|
141
|
+
# beyond the names it spells, or is refined on one; or when either chain holds an external ancestor, whose
|
|
142
|
+
# own definitions the read cannot see. Issue #1979 — a class built on a core constructor call
|
|
143
|
+
# (`class Fact < Data.define(:a)`) cannot be `self`'s class or a subclass of it, so the walk leaves it out.
|
|
144
|
+
def no_protocol_override
|
|
145
|
+
classes = [@class_name]
|
|
146
|
+
RefinedDispatch::OwnMethod.each_subclass(@scope, @class_name, possible_only: true) { |subclass| classes << subclass }
|
|
147
|
+
classes.each do |class_name|
|
|
148
|
+
chain = Scope::ResolutionChain.for(@scope, class_name.to_s, :instance, :methods)
|
|
149
|
+
unprovable! if chain.truncated? || chain.wildcard_mixin? || !settled?(chain)
|
|
150
|
+
chain.search(@scope) do |entry|
|
|
151
|
+
unprovable! if entry.external? || rewritten?(entry.name) || protocol_defined?(entry.name)
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Issue #1596 — `self` handed on as a value (an argument, an interpolation, a splat, a `when` value, a literal
|
|
158
|
+
# element or key) lets the code it reaches run `self`'s methods by protocol (`to_s`, `hash`, `==`, `to_a`),
|
|
159
|
+
# which the read does not follow, so it proves nothing. Reads `node` for one, without reading anything else.
|
|
160
|
+
def no_self_value(node)
|
|
161
|
+
return unless node.is_a?(Prism::Node)
|
|
162
|
+
|
|
163
|
+
unprovable! if node.is_a?(Prism::SelfNode)
|
|
164
|
+
node.rigor_each_child { |child| no_self_value(child) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Reads `node`, adding each instance variable it writes.
|
|
168
|
+
def read(node)
|
|
169
|
+
return unless node.is_a?(Prism::Node)
|
|
170
|
+
|
|
171
|
+
unprovable! if UNPROVABLE_NODES.include?(node.class) || node.is_a?(Prism::SelfNode)
|
|
172
|
+
written = ivar_write_name(node)
|
|
173
|
+
@written << written if written
|
|
174
|
+
if node.is_a?(Prism::InstanceVariableOperatorWriteNode) && GuardRebinding.type_method_foreign?(
|
|
175
|
+
ivar_seed(node.name), node.binary_operator, @scope
|
|
176
|
+
)
|
|
177
|
+
unprovable!
|
|
178
|
+
end
|
|
179
|
+
read_call(node) if node.is_a?(Prism::CallNode)
|
|
180
|
+
# `self.m` is the one place `self` may stand: a call on it, which {#read_call} read.
|
|
181
|
+
receiver = call_receiver(node)
|
|
182
|
+
node.rigor_each_child { |child| read(child) unless child.is_a?(Prism::SelfNode) && child.equal?(receiver) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
# Whether a `refine` block anywhere in the project defines `name`, on any class: a `using` in effect where the
|
|
188
|
+
# read code sits may route the call to it (`refine Kernel do def warn …`), whatever the receiver.
|
|
189
|
+
def refined?(name)
|
|
190
|
+
@scope.discovered_refinements.any? { |_refined, methods| methods.key?(name) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def call_receiver(node) = node.is_a?(Prism::CallNode) ? node.receiver : nil
|
|
194
|
+
|
|
195
|
+
def ivar_write_name(node)
|
|
196
|
+
case node
|
|
197
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
198
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
199
|
+
node.name
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def read_call(node)
|
|
204
|
+
receiver = node.receiver
|
|
205
|
+
return self_call(node) if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
206
|
+
|
|
207
|
+
unprovable! if GuardRebinding.code_running_name?(node.name) || refined?(node.name)
|
|
208
|
+
type = receiver_type(receiver)
|
|
209
|
+
unprovable! if type.nil? || GuardRebinding.type_method_foreign?(type, node.name, @scope)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def read_def(def_node)
|
|
213
|
+
return if @visited.key?(def_node)
|
|
214
|
+
|
|
215
|
+
@visited[def_node] = true
|
|
216
|
+
unprovable! if @visited.size > DEF_BUDGET
|
|
217
|
+
read(def_node.parameters)
|
|
218
|
+
read(def_node.body)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# The `def` the name resolves to on the class and on every project subclass, or unprovable.
|
|
222
|
+
def proven_def(name)
|
|
223
|
+
unprovable! if RefinedDispatch::OwnMethod.module_name?(@scope, @class_name)
|
|
224
|
+
|
|
225
|
+
owner = definer_ahead(@class_name, name, nil)
|
|
226
|
+
unprovable! if owner.nil?
|
|
227
|
+
def_node = @scope.user_def_for(owner, name)
|
|
228
|
+
unprovable! unless def_node.is_a?(Prism::DefNode) && def_node.receiver.nil?
|
|
229
|
+
single_definition(owner, name)
|
|
230
|
+
|
|
231
|
+
# A subclass whose superclass the tables do not know is enumerated too, and its chain ends short of the
|
|
232
|
+
# owner; one that reaches no definer of its own leaves the call to its superclass.
|
|
233
|
+
RefinedDispatch::OwnMethod.each_subclass(@scope, @class_name, possible_only: true) do |subclass|
|
|
234
|
+
unprovable! unless [owner, nil].include?(definer_ahead(subclass, name, owner))
|
|
235
|
+
end
|
|
236
|
+
def_node
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Issue #1596 — the `def` the table holds must be the owner's only definition of `name`: the owner declared in
|
|
240
|
+
# one file, with one `def` of the name there (a reopening or a `class_eval` body in the same file is a second
|
|
241
|
+
# one), and no `define_method`, `attr_*` or alias of it, nor a rewrite no literal names (`T.define_method`,
|
|
242
|
+
# `T.send(:define_method, …)`, `T.class_eval` with a computed name), nor a definition the walk could not prove
|
|
243
|
+
# runs (a possible or contested one, as `DefinerResolution` reads them). The class edge files every declaring
|
|
244
|
+
# file.
|
|
245
|
+
def single_definition(owner, name)
|
|
246
|
+
Scope::ResolutionChain.record_class(@scope, owner) if Analysis::DependencyRecorder.active?
|
|
247
|
+
unprovable! if possible_definition?(owner, name)
|
|
248
|
+
bucket = @scope.discovered_parameter_envelopes[owner] || {}
|
|
249
|
+
unprovable! if bucket[[:instance, name]] == Source::ParameterEnvelope::OPAQUE
|
|
250
|
+
unprovable! if (@scope.discovered_class_sources[owner]&.size || 0) > 1
|
|
251
|
+
unprovable! if definition_count(owner, name) > 1
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def possible_definition?(owner, name)
|
|
255
|
+
discovery = @scope.discovery
|
|
256
|
+
discovery.possible_method?(owner, name, :instance) ||
|
|
257
|
+
discovery.contested?(:discovered_def_nodes, [owner, name]) ||
|
|
258
|
+
discovery.contested?(:discovered_parameter_envelopes, [owner, [:instance, name]])
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def definition_count(owner, name)
|
|
262
|
+
count = 0
|
|
263
|
+
@scope.discovered_deferred_ranges.each_value do |rows|
|
|
264
|
+
count += rows.count { |row| row[2] == name && row[3] == :instance && row[4] == owner }
|
|
265
|
+
end
|
|
266
|
+
count
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Whether `chain`'s own order stands with no second world: no order mark and no fork. `ResolutionChain#settle`
|
|
270
|
+
# answers `:chain` for an answer no world can match only then; a fork sends it to the retro world, which the
|
|
271
|
+
# block answers otherwise, or to master's order.
|
|
272
|
+
def settled?(chain)
|
|
273
|
+
chain.settle(@scope, :chain_order) { :other_world } == :chain
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def protocol_defined?(class_name)
|
|
277
|
+
refined = @scope.discovered_refinements[class_name] || {}
|
|
278
|
+
PROTOCOL_NAMES.any? { |name| defines_here?(class_name, name) || refined.key?(name) }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Whether the project rewrites `class_name`'s method table beyond the names it spells, or refines it.
|
|
282
|
+
def rewritten?(class_name)
|
|
283
|
+
bucket = @scope.discovered_parameter_envelopes[class_name]
|
|
284
|
+
!bucket.nil? && Scope::DiscoveryIndex.rewritten_surface?(bucket)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# The first project class or module on `class_name`'s settled instance chain that defines `name`, stopping
|
|
288
|
+
# at `owner`; nil when the chain ends without one. An external ancestor reached first is unprovable: its
|
|
289
|
+
# signature, or a gem without one, may define the name.
|
|
290
|
+
def definer_ahead(class_name, name, owner)
|
|
291
|
+
chain = Scope::ResolutionChain.for(@scope, class_name.to_s, :instance, :methods)
|
|
292
|
+
unprovable! if chain.truncated? || chain.wildcard_mixin? || !settled?(chain)
|
|
293
|
+
|
|
294
|
+
chain.search(@scope) do |entry|
|
|
295
|
+
unprovable! if entry.external? || rewritten?(entry.name)
|
|
296
|
+
|
|
297
|
+
entry.name if entry.name == owner || defines_here?(entry.name, name)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# A `def`, `define_method`, `attr_*` or alias of `name` in `class_name`'s own body: the discovery table holds
|
|
302
|
+
# the last three project-wide, and the `def` table the first.
|
|
303
|
+
def defines_here?(class_name, name)
|
|
304
|
+
@scope.discovered_method?(class_name, name, :instance) || !@scope.def_node_slot(class_name, name).nil?
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def receiver_type(node)
|
|
308
|
+
literal = LITERAL_CLASSES[node.class]
|
|
309
|
+
return Type::Combinator.nominal_of(literal) if literal
|
|
310
|
+
|
|
311
|
+
case node
|
|
312
|
+
when Prism::GlobalVariableReadNode then @scope.type_of(node)
|
|
313
|
+
when Prism::InstanceVariableReadNode then ivar_seed(node.name)
|
|
314
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then core_constant_type(node)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def ivar_seed(name)
|
|
319
|
+
@scope.class_ivars_for(@class_name)[name] || unprovable!
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# A constant receiver read in the caller's scope, which the callee's lexical scope may resolve otherwise: only
|
|
323
|
+
# a core or standard-library class object or instance, under a name no project class or constant ends with.
|
|
324
|
+
def core_constant_type(node)
|
|
325
|
+
type = @scope.type_of(node)
|
|
326
|
+
return nil unless type.is_a?(Type::Singleton) || type.is_a?(Type::Nominal)
|
|
327
|
+
|
|
328
|
+
loader = @scope.environment.rbs_loader
|
|
329
|
+
return nil if loader.nil? || !loader.core_or_stdlib_class?(type.class_name)
|
|
330
|
+
|
|
331
|
+
segment = node.name.to_s
|
|
332
|
+
suffix = "::#{segment}"
|
|
333
|
+
project = [@scope.discovered_classes, @scope.in_source_constants]
|
|
334
|
+
ends_with_segment = ->(name) { (name = name.to_s) == segment || name.end_with?(suffix) }
|
|
335
|
+
return nil if project.any? { |table| table.each_key.any?(&ends_with_segment) }
|
|
336
|
+
|
|
337
|
+
type
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def unprovable!
|
|
341
|
+
raise Unprovable
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# Issue #1572 — the singleton typing site's external-ahead decline. That site still answers from
|
|
6
|
+
# {Scope#singleton_def_through_ancestors}, which passes the chain's external entries over, so `extend M; extend
|
|
7
|
+
# Enumerable` typed `C.to_a` from `M#to_a` although Ruby calls `Enumerable#to_a` (`C.singleton_class.ancestors`
|
|
8
|
+
# starts `[#<Class:C>, Enumerable, M]`). The instance typing site reads `DefinerResolution`, which already
|
|
9
|
+
# declines there.
|
|
10
|
+
#
|
|
11
|
+
# {.withhold?} is true when the read must withhold (the call types `Dynamic`): the singleton chain holds no
|
|
12
|
+
# project entry that writes the name; the reader's `owner` has no project entry on it; or an external entry ahead
|
|
13
|
+
# of the later of the two may answer first, because RBS does not know it or declares the name in it (the test
|
|
14
|
+
# `DefinerResolution` applies, `ResolutionChain::Relevance.external_lacks_on?`). The later of the two, because
|
|
15
|
+
# where the chain does not stand the reader answers master's owner. An entry writes the name with its own `def`,
|
|
16
|
+
# never a copy the extends fold put on a class object (`RefinedDispatch::ClassObject.fold_copy?`, which also
|
|
17
|
+
# matches a copy by location where the two tables came from two parses of the file), so a folded `M#to_a`
|
|
18
|
+
# answers at `M`'s own position.
|
|
19
|
+
#
|
|
20
|
+
# ADR-46: the search files the class edge of the root and of every project entry ahead of the definer, and each
|
|
21
|
+
# tested external files the negative class edge on its spelling's last segment, as `DefinerResolution` does.
|
|
22
|
+
module SingletonExternalAhead
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def withhold?(scope, class_name, method_name, owner)
|
|
26
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
27
|
+
definer = first_writer_index(scope, chain, method_name)
|
|
28
|
+
return true if definer.nil?
|
|
29
|
+
|
|
30
|
+
owner_index = chain.entries.index { |entry| !entry.external? && entry.name == owner.to_s }
|
|
31
|
+
return true if owner_index.nil?
|
|
32
|
+
|
|
33
|
+
external_ahead_may_answer?(scope, chain, method_name, [definer, owner_index].max)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def first_writer_index(scope, chain, method_name)
|
|
37
|
+
index = -1
|
|
38
|
+
chain.search(scope) do |entry|
|
|
39
|
+
index += 1
|
|
40
|
+
!entry.external? && writes_name?(scope, entry, method_name)
|
|
41
|
+
end && index
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def writes_name?(scope, entry, method_name)
|
|
45
|
+
return !scope.user_def_for(entry.name, method_name).nil? unless entry.side == :singleton
|
|
46
|
+
|
|
47
|
+
!scope.singleton_def_for(entry.name, method_name).nil? &&
|
|
48
|
+
!RefinedDispatch::ClassObject.fold_copy?(scope, entry.name, method_name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def external_ahead_may_answer?(scope, chain, method_name, stop)
|
|
52
|
+
recording = Analysis::DependencyRecorder.active?
|
|
53
|
+
chain.entries.first(stop).any? do |entry|
|
|
54
|
+
next false unless entry.external?
|
|
55
|
+
|
|
56
|
+
Analysis::DependencyRecorder.read_missing(:class, entry.raw.to_s.split("::").last) if recording
|
|
57
|
+
!Scope::ResolutionChain::Relevance.external_lacks_on?(scope, entry.candidates, entry.side, method_name)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private_class_method :first_writer_index, :writes_name?, :external_ahead_may_answer?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -109,6 +109,13 @@ module Rigor
|
|
|
109
109
|
nesting&.first
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
+
# Issue #1722 — the hook-capable test for one chain entry, asked by `call.undefined-method` of a project module
|
|
113
|
+
# mixed into a core class: a module whose `self.included(base)` hook writes `base.class_eval { def x }` gives
|
|
114
|
+
# the includer methods no table records.
|
|
115
|
+
def entry_hook_capable?(scope, entry, flavor = :methods)
|
|
116
|
+
Context.new(scope, flavor).capable?(entry)
|
|
117
|
+
end
|
|
118
|
+
|
|
112
119
|
def tested_levels(chain, from, bound)
|
|
113
120
|
starts = chain.level_starts
|
|
114
121
|
size = chain.entries.size
|