rigortype 0.4.0 → 0.4.2

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 (237) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/08-understanding-errors.md +3 -2
  25. data/docs/handbook/11-sig-gen.md +6 -0
  26. data/docs/handbook/appendix-liskov.md +5 -0
  27. data/docs/install.md +87 -15
  28. data/docs/manual/02-cli-reference.md +2 -2
  29. data/docs/manual/03-configuration.md +6 -1
  30. data/docs/manual/04-diagnostics.md +2 -1
  31. data/docs/manual/06-baseline.md +2 -0
  32. data/docs/manual/07-plugins.md +1 -1
  33. data/docs/manual/08-skills.md +4 -3
  34. data/docs/manual/12-caching.md +43 -4
  35. data/docs/manual/14-rails-quickstart.md +19 -11
  36. data/docs/manual/18-removing-dead-code.md +32 -2
  37. data/docs/manual/plugins/README.md +6 -1
  38. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  39. data/docs/manual/plugins/rigor-alba.md +60 -0
  40. data/docs/manual/plugins/rigor-minitest.md +22 -4
  41. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  42. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  43. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  44. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  45. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
  47. data/lib/rigor/analysis/check_rules.rb +407 -144
  48. data/lib/rigor/analysis/dependency_recorder.rb +215 -18
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  52. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  53. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  54. data/lib/rigor/analysis/incremental.rb +29 -9
  55. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
  56. data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
  57. data/lib/rigor/analysis/incremental_session.rb +287 -50
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  59. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  60. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +43 -9
  64. data/lib/rigor/analysis/run_stats.rb +2 -2
  65. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
  68. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  69. data/lib/rigor/analysis/runner.rb +371 -49
  70. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  71. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  72. data/lib/rigor/analysis/template_unit_paths.rb +6 -4
  73. data/lib/rigor/analysis/template_units.rb +15 -5
  74. data/lib/rigor/analysis/worker_session.rb +11 -7
  75. data/lib/rigor/cache/descriptor.rb +40 -1
  76. data/lib/rigor/cache/file_digest.rb +28 -0
  77. data/lib/rigor/cache/incremental_snapshot.rb +92 -8
  78. data/lib/rigor/cache/store.rb +63 -3
  79. data/lib/rigor/cache/value_digest.rb +325 -0
  80. data/lib/rigor/cli/annotate_command.rb +5 -0
  81. data/lib/rigor/cli/baseline_command.rb +7 -18
  82. data/lib/rigor/cli/check_command.rb +85 -14
  83. data/lib/rigor/cli/doctor_command.rb +1 -1
  84. data/lib/rigor/cli/effects_report.rb +2 -2
  85. data/lib/rigor/cli/trace_command.rb +5 -0
  86. data/lib/rigor/cli/type_of_command.rb +5 -3
  87. data/lib/rigor/cli/unused_command.rb +16 -10
  88. data/lib/rigor/cli.rb +11 -3
  89. data/lib/rigor/config_audit.rb +3 -2
  90. data/lib/rigor/config_path.rb +29 -0
  91. data/lib/rigor/configuration/severity_profile.rb +11 -0
  92. data/lib/rigor/configuration.rb +52 -7
  93. data/lib/rigor/effects/plugin_facts.rb +6 -6
  94. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  95. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  96. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  97. data/lib/rigor/environment/lockfile_resolver.rb +10 -1
  98. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  99. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  100. data/lib/rigor/environment/rbs_loader.rb +284 -15
  101. data/lib/rigor/environment/required_features.rb +206 -0
  102. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  103. data/lib/rigor/environment.rb +34 -73
  104. data/lib/rigor/inference/acceptance.rb +28 -3
  105. data/lib/rigor/inference/block_parameter_binder.rb +29 -13
  106. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  107. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  108. data/lib/rigor/inference/captured_locals.rb +7 -1
  109. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  110. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  111. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  112. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  113. data/lib/rigor/inference/declaration_walk.rb +7 -0
  114. data/lib/rigor/inference/def_node_resolver.rb +26 -3
  115. data/lib/rigor/inference/definer_resolution.rb +266 -0
  116. data/lib/rigor/inference/expression_typer.rb +351 -61
  117. data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
  118. data/lib/rigor/inference/global_write_census.rb +7 -4
  119. data/lib/rigor/inference/guard_rebinding.rb +54 -13
  120. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  121. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  122. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  123. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  124. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  125. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  126. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  127. data/lib/rigor/inference/macro_block_self_type.rb +73 -14
  128. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  129. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  130. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  131. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  132. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  133. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  134. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  135. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  136. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
  137. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  138. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  139. data/lib/rigor/inference/module_function_state.rb +214 -0
  140. data/lib/rigor/inference/multi_target_binder.rb +21 -16
  141. data/lib/rigor/inference/narrowing.rb +166 -22
  142. data/lib/rigor/inference/object_mixins.rb +180 -0
  143. data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
  144. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  145. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  146. data/lib/rigor/inference/refine_census.rb +239 -0
  147. data/lib/rigor/inference/refine_self.rb +317 -0
  148. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  149. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  150. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  151. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  152. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  153. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  154. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  155. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +143 -0
  156. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  157. data/lib/rigor/inference/scope_indexer.rb +2129 -354
  158. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  159. data/lib/rigor/inference/statement_evaluator.rb +149 -32
  160. data/lib/rigor/inference/string_mutation.rb +9 -3
  161. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  162. data/lib/rigor/inference/version_guard.rb +19 -10
  163. data/lib/rigor/language_server/project_context.rb +31 -0
  164. data/lib/rigor/language_server/server.rb +18 -0
  165. data/lib/rigor/plugin/base.rb +28 -8
  166. data/lib/rigor/plugin/inflector.rb +1 -1
  167. data/lib/rigor/plugin/io_boundary.rb +124 -7
  168. data/lib/rigor/plugin/isolation.rb +1 -1
  169. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  170. data/lib/rigor/plugin/trust_policy.rb +3 -3
  171. data/lib/rigor/project_environment.rb +29 -6
  172. data/lib/rigor/protection/discovery_seed.rb +18 -9
  173. data/lib/rigor/protection/mutation_cache.rb +4 -4
  174. data/lib/rigor/protection/mutator.rb +1 -1
  175. data/lib/rigor/reflection/constant_ancestors.rb +211 -46
  176. data/lib/rigor/reflection/constant_path.rb +33 -7
  177. data/lib/rigor/reflection.rb +41 -10
  178. data/lib/rigor/scope/discovery_index.rb +258 -3
  179. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  180. data/lib/rigor/scope/resolution_chain.rb +1032 -0
  181. data/lib/rigor/scope.rb +480 -328
  182. data/lib/rigor/sig_gen/alias_index.rb +1 -1
  183. data/lib/rigor/sig_gen/classification.rb +4 -1
  184. data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
  185. data/lib/rigor/sig_gen/generator.rb +77 -21
  186. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  187. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  188. data/lib/rigor/source/alias_names.rb +52 -0
  189. data/lib/rigor/source/literals.rb +3 -2
  190. data/lib/rigor/source/symbol_name.rb +20 -0
  191. data/lib/rigor/type/combinator.rb +59 -3
  192. data/lib/rigor/type/float_range.rb +4 -1
  193. data/lib/rigor/type/nominal.rb +6 -1
  194. data/lib/rigor/version.rb +1 -1
  195. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  196. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  197. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  198. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  199. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  200. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  201. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  202. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  203. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  204. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  207. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  208. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  209. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  210. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  211. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  212. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  213. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  214. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  215. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  216. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  217. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
  218. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  219. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  220. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  221. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  222. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  223. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  224. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  225. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  226. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  227. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  228. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  229. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  230. data/sig/rigor/environment.rbs +2 -0
  231. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  232. data/sig/rigor/scope.rbs +69 -4
  233. data/sig/rigor/source.rbs +4 -0
  234. data/sig/rigor.rbs +3 -1
  235. data/skills/rigor-project-init/SKILL.md +37 -11
  236. data/skills/rigor-project-init/references/02-configure.md +7 -2
  237. metadata +90 -1
@@ -30,10 +30,11 @@ module Rigor
30
30
  #
31
31
  # There is deliberately NO memo here. The dispatch consumer this module is being extracted FOR does
32
32
  # not exist yet, and a memo keyed on the tables this slice happens to name would be keyed on less
33
- # than the walk reads — `Scope#resolve_ancestor_class_name` consults `discovered_def_nodes` and
34
- # `discovered_methods` through `known_user_class?`, and `ancestor_name_candidates` reads
35
- # `discovered_header_nestings`. The slice that brings the hot path brings the memo, keyed on what
36
- # that consumer actually reads.
33
+ # than the walk reads — the ancestor chain resolves names through `known_user_class?`
34
+ # (`discovered_def_nodes`, `discovered_methods`) and `ancestor_name_candidates`
35
+ # (`discovered_header_nestings`). The chain itself is memoised per discovery index
36
+ # ({Scope::ResolutionChain}); the slice that brings the hot path brings this module's memo, keyed on
37
+ # what that consumer actually reads.
37
38
  module ExternalAncestorResolution
38
39
  # The owner a name must PRECEDE for its declaration to win an MRO. See {declared_before_object?}.
39
40
  OBJECT_OWNER = "Object"
@@ -67,13 +68,12 @@ module Rigor
67
68
  # `include Enumerable` so that slice 2's measurement stays its own, and the narrowing belongs to
68
69
  # the CALLER rather than to this module: the implicit-self veto must keep walking both edges,
69
70
  # because Ruby reaches an included module's methods too.
70
- # rubocop:disable-next Metrics/ParameterLists
71
- def resolve(class_name, method_name, kind = :instance, scope:, environment: nil, name_memo: nil,
72
- record_dependencies: true, mixins: true)
71
+ def resolve(class_name, method_name, kind = :instance, scope:, environment: nil, record_dependencies: true,
72
+ mixins: true)
73
73
  return nil if class_name.nil? || scope.nil?
74
74
  return nil unless kind == :instance
75
75
 
76
- compute(class_name, method_name, scope, environment, name_memo, record_dependencies, mixins)
76
+ compute(class_name, method_name, scope, environment, record_dependencies, mixins)
77
77
  end
78
78
 
79
79
  # The RBS method definition for `class_name`, or nil for a class the environment does not know, a
@@ -133,7 +133,7 @@ module Rigor
133
133
  []
134
134
  end
135
135
 
136
- def compute(class_name, method_name, scope, environment, name_memo, record_dependencies, mixins)
136
+ def compute(class_name, method_name, scope, environment, record_dependencies, mixins)
137
137
  kind = :instance
138
138
  own = method_definition(class_name, method_name, kind, scope: scope, environment: environment)
139
139
  if own
@@ -144,7 +144,7 @@ module Rigor
144
144
  end
145
145
  end
146
146
 
147
- groups = ancestor_candidate_groups(scope, class_name, name_memo, record_dependencies, mixins)
147
+ groups = ancestor_candidate_groups(scope, class_name, record_dependencies, mixins)
148
148
  farther_ancestors = {}
149
149
  groups.each_with_index do |candidates, index|
150
150
  answer = first_known_candidate_answer(candidates, method_name, kind, scope, environment)
@@ -244,14 +244,11 @@ module Rigor
244
244
  # The walk, with its ADR-46 reads attached or detached. `withhold` returns `[result, read_set]`
245
245
  # and the read set is dropped: a caller that suppresses is saying these reads are not a dependency
246
246
  # of its answer, not that they should be replayed somewhere else.
247
- def ancestor_candidate_groups(scope, class_name, name_memo, record_dependencies, mixins)
248
- memo = name_memo || {}
249
- if record_dependencies
250
- return scope.external_ancestor_name_candidates(class_name, name_memo: memo, mixins: mixins)
251
- end
247
+ def ancestor_candidate_groups(scope, class_name, record_dependencies, mixins)
248
+ return scope.external_ancestor_name_candidates(class_name, mixins: mixins) if record_dependencies
252
249
 
253
250
  Analysis::DependencyRecorder.withhold do
254
- scope.external_ancestor_name_candidates(class_name, name_memo: memo, mixins: mixins)
251
+ scope.external_ancestor_name_candidates(class_name, mixins: mixins)
255
252
  end.first
256
253
  end
257
254
  private_class_method :ancestor_candidate_groups
@@ -82,7 +82,7 @@ module Rigor
82
82
 
83
83
  # A standalone walk, for a file no existing descent visits (a `pre_eval:` entry).
84
84
  def scan(root)
85
- collector = Collector.new
85
+ collector = Collector.new(InEffectRefinements.new(root))
86
86
  walk(root, collector, true)
87
87
  collector.census.freeze
88
88
  end
@@ -98,13 +98,16 @@ module Rigor
98
98
 
99
99
  # Accumulates one file's census, fed each node of a pre-order descent. `top_level` says whether the node sits
100
100
  # outside every `class` / `module` body; it is the host walk's to track. `refine` blocks are remembered by
101
- # offset as they are met, so a node inside one is known without the host walk tracking it.
101
+ # offset as they are met, so a node inside one is known without the host walk tracking it. `in_effect` is the
102
+ # file's {InEffectRefinements}: a `refine` it places where `self` is a class is the class's own method, so its
103
+ # block is not a refinement (issue #1689).
102
104
  class Collector
103
105
  attr_reader :census
104
106
 
105
- def initialize
107
+ def initialize(in_effect)
106
108
  @census = Set.new
107
109
  @refine_ranges = []
110
+ @in_effect = in_effect
108
111
  end
109
112
 
110
113
  # A node the collector cannot read must not fail the analysis of its file, nor of every file the pre-pass
@@ -223,7 +226,7 @@ module Rigor
223
226
  def self_receiver?(node) = node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
224
227
 
225
228
  def remember_refine(node)
226
- return unless node.name == :refine && self_receiver?(node) && node.block.is_a?(Prism::BlockNode)
229
+ return unless ScopeIndexer.refine_call?(node) && !@in_effect.class_body_refine?(node)
227
230
 
228
231
  location = node.block.location
229
232
  @refine_ranges << (location.start_offset...location.end_offset)
@@ -33,12 +33,19 @@ module Rigor
33
33
  # The accepted gap is implicit conversion: a core method that calls back into a project method the program
34
34
  # does not spell (`puts obj` runs `obj.to_s`, `hash[obj]` runs `obj.hash`, `a.sort` runs `<=>`) is read as the
35
35
  # core method alone.
36
+ #
37
+ # Issue #1446 — a class guard's narrowing of an instance variable is restored at the same code, since code that
38
+ # may rebind a global may reach `self` and rebind the variable too, and at two more: `instance_variable_set` and
39
+ # `remove_instance_variable` on any receiver, and a write to the narrowed variable in code the scan reads (a
40
+ # block that may run again, a loop's next pass).
36
41
  module GuardRebinding
37
42
  # Calls that run code chosen by name or by a String.
38
43
  CODE_RUNNING_NAMES = Set[
39
44
  :send, :__send__, :public_send, :eval, :require, :require_relative, :load,
40
45
  # These rebind a constant on any receiver (`Object.const_set(:SEP, nil)`).
41
- :const_set, :remove_const
46
+ :const_set, :remove_const,
47
+ # These rebind an instance variable of a receiver that may be `self` (#1446).
48
+ :instance_variable_set, :remove_instance_variable
42
49
  ].freeze
43
50
  # These run their literal block, which the scan reads as any block, or a String of code, which it cannot read.
44
51
  BLOCK_OR_CODE_NAMES = Set[
@@ -109,10 +116,11 @@ module Rigor
109
116
  (call_node.block.is_a?(Prism::BlockArgumentNode) && may_rebind?(call_node.block, scope))
110
117
  end
111
118
 
112
- # True when running `node` may rebind one: it writes a global or constant, yields, calls `super`, or holds a
113
- # call, spelled or implicit ({.implicit_call_may_rebind?}), whose method may run foreign code. A `def` and a
114
- # lambda literal run nothing where they are written. Each node is visited once: a call's literal block is
115
- # reached as one of its children, so a nested block chain costs its size, not its depth's power.
119
+ # True when running `node` may rebind one: it writes a global, a constant or an instance variable a class guard
120
+ # narrowed, yields, calls `super`, or holds a call, spelled or implicit ({.implicit_call_may_rebind?}), whose
121
+ # method may run foreign code. A `def` and a lambda literal run nothing where they are written. Each node is
122
+ # visited once: a call's literal block is reached as one of its children, so a nested block chain costs its
123
+ # size, not its depth's power.
116
124
  #
117
125
  # A local the scanned code writes has no binding in `scope` yet, so the scan reads it as the code writes it
118
126
  # ({ScanScope.with_scanned_locals}): `copy = $sep; copy.length` is a `String` call. A literal block's parameters
@@ -126,6 +134,7 @@ module Rigor
126
134
  def scan(node, scope)
127
135
  return false unless node.is_a?(Prism::Node)
128
136
  return true if REBINDING_NODES.include?(node.class)
137
+ return true if guarded_ivar_write?(node, scope)
129
138
  return false if node.is_a?(Prism::DefNode) || node.is_a?(Prism::LambdaNode)
130
139
  return scan_call(node, scope) if node.is_a?(Prism::CallNode)
131
140
  return true if IMPLICIT_CALL_NODES.include?(node.class) && implicit_call_may_rebind?(node, scope)
@@ -135,6 +144,18 @@ module Rigor
135
144
  found
136
145
  end
137
146
 
147
+ # Issue #1446 — true when `node` writes an instance variable a class guard narrowed
148
+ # ({Scope#guard_narrowed_ivar?}). A write to any other instance variable rebinds nothing a guard recorded.
149
+ def guarded_ivar_write?(node, scope)
150
+ case node
151
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
152
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
153
+ scope.guard_narrowed_ivar?(node.name)
154
+ else
155
+ false
156
+ end
157
+ end
158
+
138
159
  def scan_call(node, scope)
139
160
  return true if call_runs_foreign_code?(node, scope)
140
161
  return true if scan(node.receiver, scope) || scan(node.arguments, scope)
@@ -156,7 +177,9 @@ module Rigor
156
177
  def implicit_call_may_rebind?(node, scope)
157
178
  kind = VARIABLE_OPERATOR_WRITES[node.class]
158
179
  return type_method_foreign?(variable_type(kind, node.name, scope), node.binary_operator, scope) if kind
159
- return type_method_foreign?(scope.type_of(node.collection), :each, scope) if node.is_a?(Prism::ForNode)
180
+ if node.is_a?(Prism::ForNode)
181
+ return type_method_foreign?(ScanScope.dispatch_type(node.collection, scope), :each, scope)
182
+ end
160
183
 
161
184
  compound_write_foreign?(node, scope)
162
185
  rescue StandardError
@@ -174,7 +197,7 @@ module Rigor
174
197
  end
175
198
 
176
199
  def compound_receiver_type(node, scope)
177
- return scope.type_of(node.receiver) if node.receiver
200
+ return ScanScope.dispatch_type(node.receiver, scope) if node.receiver
178
201
 
179
202
  scope.self_type || Type::Combinator.nominal_of("Object")
180
203
  end
@@ -264,7 +287,7 @@ module Rigor
264
287
  return ProjectMethodOwnership.targets(self_type)
265
288
  end
266
289
 
267
- ProjectMethodOwnership.targets(scope.type_of(receiver))
290
+ ProjectMethodOwnership.targets(ScanScope.dispatch_type(receiver, scope))
268
291
  end
269
292
 
270
293
  def foreign_target?(class_name, method_name, kind, scope)
@@ -300,8 +323,8 @@ module Rigor
300
323
  owner = definition.respond_to?(:defined_in) ? definition.defined_in : nil
301
324
  owner&.to_s&.delete_prefix("::")
302
325
  end
303
- private_class_method :scan, :scan_call, :receiver_targets, :foreign_target?, :universal_delegate_foreign?,
304
- :method_owner, :deferred_block_call?,
326
+ private_class_method :scan, :guarded_ivar_write?, :scan_call, :receiver_targets, :foreign_target?,
327
+ :universal_delegate_foreign?, :method_owner, :deferred_block_call?,
305
328
  :compound_write_foreign?,
306
329
  :compound_receiver_type, :compound_accessors, :compound_read_type, :variable_type,
307
330
  :type_method_foreign?
@@ -376,18 +399,36 @@ module Rigor
376
399
  bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
377
400
  end
378
401
 
379
- # What the method `call_node` calls yields its block, by position, or `[]` when that cannot be read.
402
+ # What the method `call_node` calls yields its block, by position, or `[]` when that cannot be read. A receiver
403
+ # a class guard left `bot` yields what the guarded class does ({.dispatch_type}, #1446).
380
404
  def yielded_types(call_node, scope)
381
- receiver = call_node.receiver ? scope.type_of(call_node.receiver) : scope.self_type
405
+ receiver = call_node.receiver ? dispatch_type(call_node.receiver, scope) : scope.self_type
382
406
  arguments = call_node.arguments&.arguments || []
383
407
  MethodDispatcher.expected_block_param_types(
384
408
  receiver_type: receiver, method_name: call_node.name, environment: scope.environment, scope: scope,
385
- arg_types: arguments.map { |argument| scope.type_of(argument) }
409
+ arg_types: arguments.map { |argument| scope.type_of(argument) }, call_node: call_node
386
410
  )
387
411
  rescue StandardError
388
412
  []
389
413
  end
390
414
 
415
+ # Issue #1446 — the type a call on the receiver `node` dispatches on: its type in `scope`, or, where a class
416
+ # guard disjoint from the receiver's binding left it `bot` (`return unless @io.is_a?(StringIO)` on an `IO`),
417
+ # the classes the guard named ({Narrowing.bot_guard_classes_of}), since the value that passes the guard is one
418
+ # of them. So `@io.rewind` reads as `StringIO#rewind` and keeps the narrowing, while `@cb.call` past
419
+ # `@cb.is_a?(Proc)` reads as a call on a code object, and a method a project subclass or reopening defines on
420
+ # the class still counts. A `bot` no class guard produced stays `bot`, which no target resolves, so a call on
421
+ # it counts as an unresolved callee.
422
+ def dispatch_type(node, scope)
423
+ type = scope.type_of(node)
424
+ return type unless type.is_a?(Type::Bot)
425
+
426
+ classes = Narrowing.bot_guard_classes_of(node, scope)
427
+ return type if classes.nil?
428
+
429
+ Type::Combinator.union(*classes.map { |name| Type::Combinator.nominal_of(name) })
430
+ end
431
+
391
432
  # The names a block's parameter list declares: every kind of parameter, a destructured one's parts included,
392
433
  # and a `;`-block-local. A default value is not read.
393
434
  def collect_parameter_names(node, names)
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ class InEffectRefinements
8
+ # Issue #1666 — the Proc literals whose body a `.refined` receiver call applies to, recognised from syntax.
9
+ module ProcLiterals
10
+ # The `Kernel` calls whose literal block is the Proc they return.
11
+ BARE_PROC_CALLS = %i[proc lambda].freeze
12
+ private_constant :BARE_PROC_CALLS
13
+
14
+ # Copies that keep a refined Proc's refinements (`test_refined_preserved_by_dup`): a chain reads through them.
15
+ COPY_CALLS = %i[dup clone].freeze
16
+ private_constant :COPY_CALLS
17
+
18
+ module_function
19
+
20
+ # `[literal, refined calls]` for the outermost `.refined` call `node`: the Proc literal the chain starts from
21
+ # ({.refinable}, or nil), and every `.refined` call of the chain, innermost (first called) first. The chain
22
+ # reads through parentheses and through `dup` / `clone`.
23
+ def refined_chain(node)
24
+ calls = [node]
25
+ receiver = unwrap(node.receiver)
26
+ loop do
27
+ if refined_call?(receiver)
28
+ calls << receiver
29
+ elsif !copy_call?(receiver)
30
+ break
31
+ end
32
+ receiver = unwrap(receiver.receiver)
33
+ end
34
+ [refinable(receiver), calls.reverse]
35
+ end
36
+
37
+ def refined_call?(node) = node.is_a?(Prism::CallNode) && node.name == :refined
38
+
39
+ def copy_call?(node)
40
+ node.is_a?(Prism::CallNode) && COPY_CALLS.include?(node.name) && node.arguments.nil? && node.block.nil? &&
41
+ !node.receiver.nil?
42
+ end
43
+
44
+ # `( expr )` with one statement reads as `expr`.
45
+ def unwrap(node)
46
+ while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode) &&
47
+ node.body.body.size == 1
48
+ node = node.body.body.first
49
+ end
50
+ node
51
+ end
52
+
53
+ # The node whose span is the body `node.refined(…)` applies to: `node` itself for a lambda literal, or the
54
+ # literal block of a `proc` / `lambda` call on no receiver or on `Kernel`, or of `Proc.new`; nil for anything
55
+ # else. A Proc held in a variable, or one passed as `&blk`, is not one: which block it holds is not syntax.
56
+ def refinable(node)
57
+ case node
58
+ when Prism::LambdaNode then node
59
+ when Prism::CallNode
60
+ block = node.block
61
+ return nil unless block.is_a?(Prism::BlockNode)
62
+
63
+ block if bare_proc_call?(node) || proc_new_call?(node)
64
+ end
65
+ end
66
+
67
+ def bare_proc_call?(node)
68
+ BARE_PROC_CALLS.include?(node.name) && (node.receiver.nil? || top_constant?(node.receiver, :Kernel))
69
+ end
70
+
71
+ def proc_new_call?(node)
72
+ node.name == :new && top_constant?(node.receiver, :Proc)
73
+ end
74
+
75
+ # `Name` or `::Name`.
76
+ def top_constant?(node, name)
77
+ (node.is_a?(Prism::ConstantReadNode) || (node.is_a?(Prism::ConstantPathNode) && node.parent.nil?)) &&
78
+ node.name == name
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ class InEffectRefinements
6
+ # Issue #1664 — the `def`s one file's refine bodies define, keyed by refining module, refined class (each name
7
+ # the `refine` argument's spelling can denote) and method, with the `Module.nesting` each is written in. The
8
+ # last `def` of a name wins, as Ruby's method table keeps it.
9
+ class RefineDefs
10
+ # ADR-121 WD7 — what {#lookup} answers for a name whose last definer has no body (`define_method`, `attr_*`,
11
+ # `undef`): the search for a body ends there, so the typed arm answers `Dynamic[top]` rather than another
12
+ # file's `def`.
13
+ BODILESS = :bodiless
14
+ # Issue #1749 — what {#lookup} answers for a name whose refinement the walk cannot attribute: one from a
15
+ # `refine` that runs inside a method (it exists once the method has run, replacing what a refine body wrote for
16
+ # the name, or adding nothing if it never runs, and `using` sees it only if it runs first), or from an eval on
17
+ # a constant that several names denote (it runs on one of them, which another file decides, and replaces or is
18
+ # replaced by a `def` of the name in a refine body of the module, whichever runs first). A `def` of the name
19
+ # in a module-body refine, in this file or another, cannot say which one answers, so the name is never typed
20
+ # from any body: the lookup ends there across every file that declares the module, and unlike {BODILESS} a
21
+ # later record does not replace it.
22
+ UNDECIDED = :undecided
23
+
24
+ attr_reader :nestings
25
+
26
+ def initialize
27
+ @nodes = {}
28
+ @nestings = {}.compare_by_identity
29
+ end
30
+
31
+ # `owners` are the refining modules (several where an eval on a constant may run on any of its names), each
32
+ # filed under every class of `targets`. `name` is the method `def_node` answers for: its own name, or an
33
+ # alias's (ADR-121 WD7). A nil `def_node` is a later definer with no body (`define_method`, `attr_*`, `undef`),
34
+ # which replaces the earlier `def`. A name whose refinement the walk cannot attribute keeps no body and is
35
+ # {UNDECIDED}: several owners (Ruby runs the body on one of them only), or `created`, true for a `refine` that
36
+ # runs inside a method.
37
+ def record(owners, targets, name, def_node, nesting, created: false)
38
+ undecided = created || owners.size > 1
39
+ def_node = nil if undecided
40
+ owners.each do |owner|
41
+ by_class = (@nodes[owner] ||= {})
42
+ targets.each do |class_name|
43
+ by_name = (by_class[class_name] ||= {})
44
+ by_name[name] = undecided ? UNDECIDED : (def_node || BODILESS) unless by_name[name] == UNDECIDED
45
+ end
46
+ end
47
+ @nestings[def_node] = nesting if def_node
48
+ end
49
+
50
+ def lookup(module_name, class_name, method_name) = @nodes.dig(module_name, class_name, method_name)
51
+ end
52
+ end
53
+ end
54
+ end