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.
Files changed (172) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/core_overlay/ostruct.rbs +40 -0
  4. data/docs/handbook/11-sig-gen.md +104 -15
  5. data/docs/manual/02-cli-reference.md +43 -12
  6. data/docs/manual/03-configuration.md +20 -7
  7. data/docs/manual/04-diagnostics.md +56 -3
  8. data/docs/manual/05-inspecting-types.md +2 -2
  9. data/docs/manual/06-baseline.md +1 -1
  10. data/docs/manual/07-plugins.md +3 -2
  11. data/docs/manual/08-skills.md +7 -0
  12. data/docs/manual/10-mcp-server.md +40 -4
  13. data/docs/manual/18-removing-dead-code.md +23 -2
  14. data/docs/manual/plugins/rigor-rails-routes.md +2 -2
  15. data/docs/manual/plugins/rigor-rspec.md +101 -1
  16. data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
  17. data/exe/rigor +12 -17
  18. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  19. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
  20. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
  21. data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
  22. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  23. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
  24. data/lib/rigor/analysis/check_rules.rb +274 -51
  25. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  26. data/lib/rigor/analysis/incremental.rb +61 -0
  27. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  28. data/lib/rigor/analysis/incremental_session.rb +270 -28
  29. data/lib/rigor/analysis/path_expansion.rb +3 -1
  30. data/lib/rigor/analysis/reachability/graph.rb +111 -14
  31. data/lib/rigor/analysis/reachability/scan.rb +305 -23
  32. data/lib/rigor/analysis/rule_catalog.rb +22 -4
  33. data/lib/rigor/analysis/run_cache_key.rb +31 -7
  34. data/lib/rigor/analysis/run_cache_probe.rb +5 -3
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
  36. data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
  37. data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
  38. data/lib/rigor/analysis/runner.rb +102 -72
  39. data/lib/rigor/analysis/severity_stamp.rb +45 -1
  40. data/lib/rigor/analysis/worker_session.rb +69 -2
  41. data/lib/rigor/cache/descriptor.rb +79 -12
  42. data/lib/rigor/cache/engine_source.rb +3 -2
  43. data/lib/rigor/cache/incremental_snapshot.rb +39 -5
  44. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  45. data/lib/rigor/cache/store.rb +4 -3
  46. data/lib/rigor/cli/command.rb +3 -1
  47. data/lib/rigor/cli/docs_command.rb +3 -2
  48. data/lib/rigor/cli/doctor_command.rb +1 -1
  49. data/lib/rigor/cli/explain_command.rb +3 -2
  50. data/lib/rigor/cli/mcp_command.rb +1 -1
  51. data/lib/rigor/cli/plugin_command.rb +2 -1
  52. data/lib/rigor/cli/plugins_command.rb +2 -1
  53. data/lib/rigor/cli/sig_gen_command.rb +32 -11
  54. data/lib/rigor/cli/skill_command.rb +2 -1
  55. data/lib/rigor/cli/skill_describe.rb +3 -2
  56. data/lib/rigor/cli/unused_command.rb +10 -7
  57. data/lib/rigor/cli.rb +3 -2
  58. data/lib/rigor/config_audit.rb +163 -22
  59. data/lib/rigor/configuration/dependencies.rb +19 -11
  60. data/lib/rigor/configuration/severity_profile.rb +20 -2
  61. data/lib/rigor/configuration.rb +29 -3
  62. data/lib/rigor/effects/signature_sources.rb +3 -1
  63. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  64. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  65. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  66. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  67. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  68. data/lib/rigor/environment/rbs_loader.rb +67 -11
  69. data/lib/rigor/inference/block_call_timing.rb +12 -1
  70. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  71. data/lib/rigor/inference/captured_locals.rb +67 -13
  72. data/lib/rigor/inference/clause_widening.rb +90 -0
  73. data/lib/rigor/inference/definer_resolution.rb +101 -25
  74. data/lib/rigor/inference/expression_typer.rb +94 -9
  75. data/lib/rigor/inference/guard_rebinding.rb +7 -2
  76. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  77. data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
  78. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  79. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
  80. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  81. data/lib/rigor/inference/method_dispatcher.rb +67 -3
  82. data/lib/rigor/inference/module_function_state.rb +213 -51
  83. data/lib/rigor/inference/narrowing.rb +235 -75
  84. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  85. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  86. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  87. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  88. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  89. data/lib/rigor/inference/project_seed_tables.rb +120 -0
  90. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  91. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  92. data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
  93. data/lib/rigor/inference/repeated_or_writes.rb +414 -1
  94. data/lib/rigor/inference/return_barrier.rb +16 -0
  95. data/lib/rigor/inference/scope_indexer.rb +1061 -122
  96. data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
  97. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  98. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  99. data/lib/rigor/inference/statement_evaluator.rb +512 -150
  100. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  101. data/lib/rigor/lens/anchor.rb +3 -3
  102. data/lib/rigor/lens/contract_reader.rb +60 -2
  103. data/lib/rigor/lens/document.rb +4 -3
  104. data/lib/rigor/lens/file_reader.rb +452 -73
  105. data/lib/rigor/lens/members.rb +81 -17
  106. data/lib/rigor/lens/run.rb +73 -4
  107. data/lib/rigor/lens.rb +2 -1
  108. data/lib/rigor/mcp/server.rb +63 -22
  109. data/lib/rigor/plugin/base.rb +3 -2
  110. data/lib/rigor/plugin/box_probe.rb +154 -7
  111. data/lib/rigor/plugin/io_boundary.rb +42 -6
  112. data/lib/rigor/plugin/isolation.rb +4 -3
  113. data/lib/rigor/project_environment.rb +6 -3
  114. data/lib/rigor/protection/discovery_seed.rb +9 -36
  115. data/lib/rigor/protection/mutation_cache.rb +8 -2
  116. data/lib/rigor/rooted_glob.rb +27 -0
  117. data/lib/rigor/scope/discovery_index.rb +5 -0
  118. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  119. data/lib/rigor/scope/resolution_chain.rb +40 -5
  120. data/lib/rigor/scope.rb +56 -8
  121. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  122. data/lib/rigor/sig_gen/classification.rb +4 -1
  123. data/lib/rigor/sig_gen/generator.rb +167 -22
  124. data/lib/rigor/sig_gen/layout_index.rb +40 -1
  125. data/lib/rigor/sig_gen/method_candidate.rb +8 -0
  126. data/lib/rigor/sig_gen/observation_collector.rb +137 -11
  127. data/lib/rigor/sig_gen/path_mapper.rb +18 -2
  128. data/lib/rigor/sig_gen/renderer.rb +13 -155
  129. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  130. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  131. data/lib/rigor/sig_gen/write_result.rb +3 -1
  132. data/lib/rigor/sig_gen/writer.rb +50 -35
  133. data/lib/rigor/signature_path_audit.rb +4 -2
  134. data/lib/rigor/type/constant.rb +10 -1
  135. data/lib/rigor/version.rb +1 -1
  136. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  137. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
  138. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  139. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  140. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  141. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  142. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  143. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  144. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  145. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  146. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  147. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
  148. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  149. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  150. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  151. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
  152. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  153. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
  154. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
  155. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  156. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  157. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  158. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  159. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  160. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  161. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  164. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  166. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  167. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  168. data/sig/rigor/rooted_glob.rbs +7 -0
  169. data/sig/rigor/scope.rbs +4 -3
  170. data/skills/rigor-doctor/references/01-checks.md +23 -11
  171. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  172. 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