rigortype 0.4.1 → 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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  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/11-sig-gen.md +6 -0
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/03-configuration.md +1 -1
  27. data/docs/manual/04-diagnostics.md +2 -1
  28. data/docs/manual/06-baseline.md +2 -0
  29. data/docs/manual/07-plugins.md +1 -1
  30. data/docs/manual/18-removing-dead-code.md +32 -2
  31. data/docs/manual/plugins/README.md +6 -1
  32. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  33. data/docs/manual/plugins/rigor-alba.md +60 -0
  34. data/docs/manual/plugins/rigor-minitest.md +22 -4
  35. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  36. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  37. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  38. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  39. data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
  40. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  41. data/lib/rigor/analysis/check_rules.rb +289 -57
  42. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  43. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  44. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  45. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  46. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  47. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  48. data/lib/rigor/analysis/incremental.rb +29 -9
  49. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  50. data/lib/rigor/analysis/incremental_run_slot.rb +3 -1
  51. data/lib/rigor/analysis/incremental_session.rb +14 -1
  52. data/lib/rigor/analysis/reachability/graph.rb +123 -23
  53. data/lib/rigor/analysis/reachability/scan.rb +290 -36
  54. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  55. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  56. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +87 -5
  57. data/lib/rigor/analysis/runner/pool_coordinator.rb +50 -11
  58. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  59. data/lib/rigor/analysis/runner.rb +34 -3
  60. data/lib/rigor/analysis/worker_session.rb +7 -4
  61. data/lib/rigor/cache/incremental_snapshot.rb +22 -0
  62. data/lib/rigor/cli/annotate_command.rb +5 -0
  63. data/lib/rigor/cli/baseline_command.rb +7 -18
  64. data/lib/rigor/cli/check_command.rb +2 -1
  65. data/lib/rigor/cli/trace_command.rb +5 -0
  66. data/lib/rigor/cli/type_of_command.rb +5 -3
  67. data/lib/rigor/cli/unused_command.rb +13 -7
  68. data/lib/rigor/cli.rb +10 -2
  69. data/lib/rigor/configuration/severity_profile.rb +6 -0
  70. data/lib/rigor/configuration.rb +46 -2
  71. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  72. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  73. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  74. data/lib/rigor/environment/rbs_loader.rb +283 -14
  75. data/lib/rigor/environment/required_features.rb +206 -0
  76. data/lib/rigor/environment.rb +21 -3
  77. data/lib/rigor/inference/acceptance.rb +28 -3
  78. data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
  79. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  81. data/lib/rigor/inference/expression_typer.rb +215 -16
  82. data/lib/rigor/inference/global_write_census.rb +7 -4
  83. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  84. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  85. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  86. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
  87. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  88. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  89. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  90. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  91. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  92. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  93. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  94. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  95. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  96. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  97. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  98. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  99. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  100. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +259 -28
  101. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  102. data/lib/rigor/inference/method_dispatcher.rb +44 -11
  103. data/lib/rigor/inference/module_function_state.rb +2 -1
  104. data/lib/rigor/inference/narrowing.rb +20 -6
  105. data/lib/rigor/inference/object_mixins.rb +180 -0
  106. data/lib/rigor/inference/project_patched_methods.rb +10 -5
  107. data/lib/rigor/inference/project_patched_scanner.rb +71 -3
  108. data/lib/rigor/inference/refine_census.rb +239 -0
  109. data/lib/rigor/inference/refine_self.rb +317 -0
  110. data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
  111. data/lib/rigor/inference/refined_dispatch.rb +272 -0
  112. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  113. data/lib/rigor/inference/scope_indexer.rb +687 -60
  114. data/lib/rigor/inference/statement_evaluator.rb +71 -20
  115. data/lib/rigor/inference/string_mutation.rb +9 -3
  116. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  117. data/lib/rigor/inference/version_guard.rb +19 -10
  118. data/lib/rigor/language_server/project_context.rb +31 -0
  119. data/lib/rigor/language_server/server.rb +18 -0
  120. data/lib/rigor/plugin/inflector.rb +1 -1
  121. data/lib/rigor/plugin/io_boundary.rb +42 -3
  122. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  123. data/lib/rigor/project_environment.rb +29 -6
  124. data/lib/rigor/protection/mutation_cache.rb +1 -1
  125. data/lib/rigor/protection/mutator.rb +1 -1
  126. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  127. data/lib/rigor/reflection/constant_path.rb +13 -2
  128. data/lib/rigor/reflection.rb +40 -8
  129. data/lib/rigor/scope/discovery_index.rb +40 -1
  130. data/lib/rigor/scope/resolution_chain.rb +159 -11
  131. data/lib/rigor/scope.rb +189 -10
  132. data/lib/rigor/sig_gen/classification.rb +4 -1
  133. data/lib/rigor/sig_gen/generator.rb +55 -5
  134. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  135. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  136. data/lib/rigor/source/alias_names.rb +52 -0
  137. data/lib/rigor/source/literals.rb +3 -2
  138. data/lib/rigor/source/symbol_name.rb +20 -0
  139. data/lib/rigor/type/combinator.rb +34 -2
  140. data/lib/rigor/type/float_range.rb +4 -1
  141. data/lib/rigor/type/nominal.rb +6 -1
  142. data/lib/rigor/version.rb +1 -1
  143. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  144. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  145. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  146. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  147. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  148. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  149. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  150. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  151. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  152. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  153. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  156. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  157. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  158. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  159. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  160. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  161. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  162. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  163. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  164. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  165. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  166. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  167. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  168. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  169. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  170. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  171. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  172. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  173. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  174. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  175. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  176. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  177. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  178. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  179. data/sig/rigor/scope.rbs +27 -4
  180. data/sig/rigor/source.rbs +4 -0
  181. data/sig/rigor.rbs +3 -1
  182. metadata +66 -1
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/constant_path"
6
+ require_relative "../source/node_children"
7
+ require_relative "refine_census"
8
+ require_relative "refine_self"
9
+ require_relative "in_effect_refinements/proc_literals"
10
+ require_relative "in_effect_refinements/scope_reads"
11
+ require_relative "in_effect_refinements/refine_defs"
12
+ require_relative "in_effect_refinements/walk"
13
+
14
+ module Rigor
15
+ module Inference
16
+ # ADR-121 WD1 (issue #1673) — the in-effect refinements of one file: at each program point, the ORDERED list of
17
+ # refining modules whose refinements Ruby applies there (`CONTEXT.md` § in-effect refinements). A later entry
18
+ # wins over an earlier one. The check rules ({Analysis::CheckRules::LexicalMethodSites}) and the typer
19
+ # ({Scope#in_effect_refinements}) both read the one `Inference::ScopeIndexer` stamps for the file, so a call
20
+ # cannot be silenced as refined and typed as unrefined.
21
+ #
22
+ # The list is built from activations, each in effect over a span of the file and placed by its activation
23
+ # offset:
24
+ #
25
+ # - **Lexical `using M`**, from the end of the call to the end of the body that holds it: the file's top level
26
+ # or a `class` / `module` / `class << …` body, including everything nested in it. An outer body's `using`s
27
+ # precede an inner body's, and a body's own `using`s keep their textual order. A `using` inside a `def`
28
+ # raises in Ruby and activates nothing. `M` is resolved lexically: each name its spelling can denote sits at
29
+ # the `using`'s position, innermost LAST, so where several are declared the one Ruby's lookup finds wins.
30
+ # The caller's expansion block puts `M`'s included modules ahead of `M` and its prepended modules after it
31
+ # (issue #1671), as CRuby activates them.
32
+ # - **A `refine X do … end` block**, over the block: its own refining module, after the enclosing `using`s.
33
+ # The module is named where the block's `self` is: the innermost `module` / `class` body, the constant a
34
+ # `M = Module.new do … end` write names, or a module's own singleton method (`def self.x`, a `def` in its
35
+ # `class << self`, issue #1749), where `self` is the module once the method is called. Anywhere else (another
36
+ # block, an instance `def`, `class << …`, the top level) the module is not named, and the block contributes
37
+ # {UNKNOWN}. Where the `refine`'s `self` is known to be a class — directly in a `class` or `class << …` body,
38
+ # or in the block of a `Class.new` / `Struct.new` / `Data.define` call — it is not `Module#refine` (`Class`
39
+ # undefines it), so it activates nothing and its block is an ordinary one (issue #1689; {#class_body_refine?}).
40
+ # - **A `using` whose argument is not a constant** (`using Module.new { … }`) names no module. It contributes
41
+ # {UNKNOWN} throughout its file, which is broader than Ruby's scoping and the declining direction.
42
+ # - **Block sources** (ADR-121 WD1's third and fourth). A Proc literal that is directly the receiver of
43
+ # `Proc#refined` (#1666) — a lambda literal, or the literal block of a bare `proc` / `lambda` call or of
44
+ # `Proc.new` — puts each `.refined` argument in effect over the literal's body, after the literal's lexical
45
+ # list and in call order along a `.refined(A).refined(B)` chain (CRuby duplicates the block's cref and appends).
46
+ # Nested blocks and literals inherit it, being inside the span. An argument that is not a constant names no
47
+ # module and contributes {UNKNOWN} over that literal's body only. A plugin-declared refined block (#1667) is
48
+ # typing-time knowledge, so its declared modules arrive as the `declared` argument of {#at} and {#for_node} and
49
+ # are appended after the block site's lexical list. Both expand through includes as a `using` does.
50
+ #
51
+ # Activating a module that is already listed changes nothing: it keeps its first position
52
+ # (`rb_using_refinement` returns early, CRuby `eval.c`; `using A; using B; using A` answers B's method).
53
+ #
54
+ # Built lazily: the walk runs the first time a consumer asks, so a file nobody asks about pays one small object.
55
+ class InEffectRefinements
56
+ # The marker an activation contributes when it may put any refinement in effect: a non-constant `using`, a
57
+ # `using` whose include expansion is unknown, or a `refine` block whose module this walk cannot name. A list
58
+ # that carries it answers "any refinement may be in effect"; its position carries no meaning.
59
+ UNKNOWN = :unknown_refinement
60
+ EMPTY = [].freeze
61
+ # ADR-121 WD7 (A1) — what a caller's expansion block answers for a name nothing declares: such a candidate of a
62
+ # `using`'s spelling is not the module Ruby's lookup finds, so it leaves the list. Never itself a list entry.
63
+ UNDECLARED = :undeclared_module
64
+
65
+ # One activation: in effect over `[start, stop)`, ordered by `order`. `names` is the module names (several
66
+ # when a lexical spelling can denote several), or nil for {UNKNOWN}; `expand` says whether the caller's
67
+ # include expansion applies (a `using`'s, not a `refine` block's own module).
68
+ Activation = Data.define(:order, :start, :stop, :names, :expand, :refine_block) do
69
+ def covers?(offset) = offset >= start && offset < stop
70
+ end
71
+ private_constant :Activation
72
+
73
+ extend ScopeReads
74
+ include Walk
75
+
76
+ def initialize(root)
77
+ @root = root
78
+ @built = false
79
+ end
80
+
81
+ # The in-effect refinements at `offset` of this file, ordered so a later activation comes later, each module
82
+ # once at its first position, then `declared` (a block source's modules) on the same terms. The block, when
83
+ # given, answers the modules `using name` puts in effect, in CRuby's activation order, or nil when it cannot
84
+ # tell, which contributes {UNKNOWN}; without it a `using` contributes its own name.
85
+ # The caller vouches that `offset` is in this file; {#for_node} checks.
86
+ def at(offset, declared = EMPTY, &expand)
87
+ build
88
+ return EMPTY if @activations.empty? && declared.empty?
89
+
90
+ list = []
91
+ @activations.each do |activation|
92
+ append_activation(list, activation, expand) if activation.covers?(offset)
93
+ end
94
+ declared.each { |name| append_expanded(list, name, expand) }
95
+ list.empty? ? EMPTY : list.freeze
96
+ end
97
+
98
+ # {#at} for a node, or only `declared` when `node` is not this file's: a callee body another file wrote,
99
+ # typed under this file's scope, finds no lexical refinement here. Its own file's are not consulted.
100
+ def for_node(node, declared = EMPTY, &)
101
+ return at(EMPTY_OFFSET, declared, &) unless member?(node)
102
+
103
+ at(node.location.start_offset, declared, &)
104
+ end
105
+
106
+ # Issue #1120 — is a refinement from one of `modules` (refining-module names) in effect at `offset`, with
107
+ # `declared` (a plugin-declared refined block's modules, issue #1667) appended as {#at} appends them? The
108
+ # silencing answer is broader than the list in two places, both the declining direction: a {UNKNOWN} entry
109
+ # counts as every module, and inside a `refine` block every module counts, not only the block's own.
110
+ def refinement_active?(offset, modules, declared = EMPTY, &)
111
+ build
112
+ return true if @unresolved_using
113
+ return true if @activations.any? { |activation| activation.refine_block && activation.covers?(offset) }
114
+
115
+ at(offset, declared, &).any? { |name| name == UNKNOWN || modules.include?(name) }
116
+ end
117
+
118
+ # Does the file activate no refinement anywhere? A consumer asks first, so a file without one pays no list.
119
+ def empty?
120
+ build
121
+ @activations.empty?
122
+ end
123
+
124
+ # Issue #1664 — the `Prism::DefNode` a `refine class_name do … end` body in this file defines `method_name` with,
125
+ # for the refining module `module_name`, or nil. The last such `def` wins, as Ruby's method table keeps it. The
126
+ # refined class is matched by any name its spelling can denote, as the refinement table records it. A name whose
127
+ # last definer has no body answers `RefineDefs::BODILESS` (ADR-121 WD7).
128
+ def refinement_def(module_name, class_name, method_name)
129
+ build
130
+ @refine_defs&.lookup(module_name, class_name, method_name)
131
+ end
132
+
133
+ # `{refine-body DefNode => Module.nesting where it is written}` for the defs {#refinement_def} answers, which a
134
+ # body re-typed from another file's parse reads its constants by (`DefNodeResolver.refinement_query`).
135
+ def refine_def_nestings
136
+ build
137
+ @refine_defs ? @refine_defs.nestings : {}
138
+ end
139
+
140
+ # Issue #1689 — is `call_node` an implicit- or `self`-receiver `refine` call with a literal block
141
+ # ({ScopeIndexer.refine_call?}, whatever its argument) whose `self` this walk knows to be a class? `Class`
142
+ # undefines `refine`, so such a call is the class's own method (a DSL a base class defines), not
143
+ # `Module#refine`: its block is an ordinary block, and its `def`s refine nothing. Every walk that recognises a
144
+ # refine body asks this, so they agree on which calls are refine bodies. A node another file's parse made
145
+ # answers false.
146
+ def class_body_refine?(call_node)
147
+ build
148
+ offsets = @class_body_refines
149
+ !offsets.nil? && member?(call_node) && offsets.include?(call_node.block.location.start_offset)
150
+ end
151
+
152
+ # ADR-121 WD7 — the `RefineSelf::Context` of the `self` the `refine`-shaped `node` (a `refine` call, a `:refine`
153
+ # literal, a String naming `refine` passed to an eval, `send` or method-naming call) runs on, as this walk worked
154
+ # it out, or nil for a node it did not record (another file's, or no such node). The census charges what this
155
+ # answers, so the table and the activations agree.
156
+ def refine_context(node)
157
+ build
158
+ @refine_contexts[node.location.start_offset] if member?(node)
159
+ end
160
+
161
+ # Is this the query over `root`'s tree?
162
+ def over?(root) = @root.equal?(root)
163
+
164
+ # Issue #1367 — is any refinement in effect at `offset`?
165
+ def any_at?(offset) = !at(offset).empty?
166
+
167
+ # Is `def_node` one of the defs a `refine X do … end` body defines on X? Such a def redefines X's method by
168
+ # design, so X's declared signature for the name is not its contract (issue #1120, maintainer ruling a′).
169
+ def refinement_def?(def_node)
170
+ build
171
+ @refinement_defs.include?(def_node.location.start_offset)
172
+ end
173
+
174
+ private
175
+
176
+ EMPTY_OFFSET = -1
177
+ EMPTY_SET = Set.new.freeze
178
+ NO_CONTEXTS = {}.freeze
179
+ private_constant :EMPTY_OFFSET, :EMPTY_SET, :NO_CONTEXTS
180
+
181
+ # A spelling's candidates are alternatives, innermost first; Ruby's lexical lookup finds the innermost one
182
+ # that exists, so it goes last and wins where several are declared. ADR-121 WD7 (A1): every declared candidate
183
+ # stays, because which one Ruby finds can depend on load order; one nothing declares
184
+ # ({UNDECLARED}) leaves; and when none is declared, the spelling enters alone, a module no table declares,
185
+ # which the readers treat as opaque (`using` of a gem's module, issue #1796).
186
+ def append_activation(list, activation, expand)
187
+ names = activation.names
188
+ if names.nil?
189
+ list << UNKNOWN unless list.include?(UNKNOWN)
190
+ return
191
+ end
192
+ unless activation.expand && expand
193
+ names.reverse_each { |name| list << name unless list.include?(name) }
194
+ return
195
+ end
196
+
197
+ declared = false
198
+ names.reverse_each do |name|
199
+ expanded = expand.call(name)
200
+ next if expanded == UNDECLARED
201
+
202
+ declared = true
203
+ append_entries(list, expanded)
204
+ end
205
+ list << names.last unless declared || list.include?(names.last)
206
+ end
207
+
208
+ # A block source's declared module (#1667), listed as declared. One nothing declares refines nothing Rigor can
209
+ # see, so it matches no row; the readers never treat a declared module as opaque.
210
+ def append_expanded(list, name, expand)
211
+ expanded = expand ? expand.call(name) : [name]
212
+ append_entries(list, expanded == UNDECLARED ? [name] : expanded)
213
+ end
214
+
215
+ def append_entries(list, expanded)
216
+ if expanded.nil?
217
+ list << UNKNOWN unless list.include?(UNKNOWN)
218
+ else
219
+ expanded.each { |entry| list << entry unless list.include?(entry) }
220
+ end
221
+ end
222
+
223
+ # Is `node` one this file's parse holds? Prism gives every node of one parse the same `Prism::Source`, so a
224
+ # node another file's parse made answers false in O(1); a node synthesised over this parse's source counts.
225
+ def member?(node)
226
+ node.send(:source).equal?(@root.send(:source))
227
+ end
228
+
229
+ def build
230
+ return if @built
231
+
232
+ @built = true
233
+ @activations = EMPTY
234
+ @refinement_defs = EMPTY_SET
235
+ @refine_defs = nil
236
+ @chained_refined_calls = nil
237
+ @class_body_refines = nil
238
+ @refine_contexts = NO_CONTEXTS
239
+ @literal_bindings = nil
240
+ @refine_offsets = nil
241
+ @unresolved_using = false
242
+ return if @root.nil? || !mentions_refinements?
243
+
244
+ @activations = []
245
+ @refinement_defs = Set.new
246
+ @refine_defs = RefineDefs.new
247
+ @refine_contexts = {}
248
+ @nesting = EMPTY
249
+ location = @root.location
250
+ walk(@root, [], [location.start_offset, location.end_offset], false, RefineSelf.top)
251
+ sort_activations
252
+ @activations.freeze
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module KeyPresenceGuard
8
+ # Issue #1703 — the syntax scans {KeyPresenceGuard} decides with: whether a guard may be relied on at all
9
+ # ({.eligible?}), whether the source between a guard and its read may have broken it ({.intervening?}), and
10
+ # whether one call or write breaks it ({.breaks?}, {.aliases_guard?}). Every answer errs towards breaking.
11
+ module Scan
12
+ # Blockless calls on a guarded receiver that cannot remove a key or rebind a slot, and do not answer the
13
+ # receiver itself (so `x = h.itself` or `x = h.to_h` is a hand-on, not a read). Any other call on it, and any
14
+ # call with a block, may break the guard (`h.delete(k)`, `h.send(:delete, k)`, `h.each { … h.clear }`).
15
+ READ_ONLY_RECEIVER_CALLS = %i[
16
+ [] key? has_key? include? member? fetch dig values_at fetch_values size length count empty? any? none?
17
+ keys values to_a dup clone frozen? hash == != eql? equal? inspect to_s is_a? kind_of? instance_of? nil?
18
+ respond_to?
19
+ ].to_set.freeze
20
+
21
+ # The nodes whose `value` a write stores: storing the receiver or the key's root somewhere else aliases it.
22
+ WRITE_NODES = [
23
+ Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
24
+ Prism::GlobalVariableWriteNode, Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::MultiWriteNode,
25
+ Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableOperatorWriteNode,
26
+ Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
27
+ Prism::InstanceVariableOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode,
28
+ Prism::IndexOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode, Prism::CallOperatorWriteNode
29
+ ].to_set.freeze
30
+
31
+ # The nodes that bind a local by name (a write, or a target of a multiple assignment, `for`, `rescue => e`, a
32
+ # pattern or a named capture) and an instance variable by name.
33
+ LOCAL_BINDERS = [
34
+ Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
35
+ Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableTargetNode
36
+ ].to_set.freeze
37
+
38
+ IVAR_BINDERS = [
39
+ Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
40
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
41
+ ].to_set.freeze
42
+
43
+ CLOSURE_NODES = [Prism::BlockNode, Prism::LambdaNode].to_set.freeze
44
+
45
+ private_constant :LOCAL_BINDERS, :IVAR_BINDERS, :CLOSURE_NODES
46
+
47
+ module_function
48
+
49
+ # A guard is relied on only when nothing in reach can change what it proved:
50
+ # - a local receiver is never handed on in its method body — as an argument, a written value, inside a block, or
51
+ # as the receiver of anything but a blockless read ({.mentions?}) — before the guard or after it;
52
+ # - an instance-variable receiver is never handed on anywhere in the file;
53
+ # - a constant receiver is assigned once in the file, a frozen hash literal (`MAP = { … }.freeze`), and is never
54
+ # handed on in the file;
55
+ # - a local key root is bound nowhere in its method body after the guard, never stored elsewhere (`j = k`) and
56
+ # never read inside a block; an instance-variable key root is bound nowhere in the file.
57
+ def eligible?(walk, call_node, guard)
58
+ kind, name, key = guard
59
+ body = walk.enclosing_def(call_node) || walk.root
60
+ receiver_eligible?(walk, body, kind, name) && key_root_eligible?(walk, body, key, call_node)
61
+ end
62
+
63
+ # Memoised per receiver and, for a local, per method body: the answer depends on neither the guard nor its key.
64
+ def receiver_eligible?(walk, body, kind, name)
65
+ memo_key = [:receiver, kind, name, kind == :local ? body.location.start_offset : nil]
66
+ return walk.memo[memo_key] if walk.memo.key?(memo_key)
67
+
68
+ walk.memo[memo_key] = scan_receiver_eligible?(walk, body, kind, name)
69
+ end
70
+
71
+ def scan_receiver_eligible?(walk, body, kind, name)
72
+ case kind
73
+ when :local then !mentions?(body, [kind, name])
74
+ when :ivar then !mentions?(walk.root, [kind, name])
75
+ when :const then frozen_constant?(walk, name) && !mentions?(walk.root, [kind, name])
76
+ else false
77
+ end
78
+ end
79
+
80
+ def frozen_constant?(walk, name)
81
+ walk.frozen_constants ||= frozen_constant_names(walk.root)
82
+ walk.frozen_constants.include?(name)
83
+ end
84
+
85
+ # The constants the file assigns exactly once, to a frozen hash literal.
86
+ def frozen_constant_names(root)
87
+ writes = Hash.new { |table, key| table[key] = [] }
88
+ collect_constant_writes(root, writes)
89
+ writes.filter_map { |name, values| name if values.size == 1 && frozen_hash_literal?(values.first) }.to_set
90
+ end
91
+
92
+ def collect_constant_writes(node, writes)
93
+ writes[node.name] << node.value if node.is_a?(Prism::ConstantWriteNode)
94
+ writes[node.name] << nil if node.is_a?(Prism::ConstantOrWriteNode) || node.is_a?(Prism::ConstantTargetNode)
95
+ node.compact_child_nodes.each { |child| collect_constant_writes(child, writes) }
96
+ end
97
+
98
+ def frozen_hash_literal?(value)
99
+ value.is_a?(Prism::CallNode) && value.name == :freeze && value.arguments.nil? && value.block.nil? &&
100
+ value.receiver.is_a?(Prism::HashNode)
101
+ end
102
+
103
+ def key_root_eligible?(walk, body, key, guard_node)
104
+ root_ref = key.root
105
+ kind, name = root_ref
106
+ if kind == :ivar
107
+ memo_key = [:ivar_key_root, name]
108
+ return walk.memo[memo_key] if walk.memo.key?(memo_key)
109
+
110
+ walk.memo[memo_key] = !binds?(walk.root, IVAR_BINDERS, name, -1) && !closure_reads?(walk.root, root_ref)
111
+ else
112
+ !binds?(body, LOCAL_BINDERS, name, guard_node.location.start_offset) &&
113
+ !aliases?(body, key) && !closure_reads?(body, root_ref)
114
+ end
115
+ end
116
+
117
+ # Whether a binder of `name` starts after `offset` within `node`.
118
+ def binds?(node, binders, name, offset)
119
+ return true if binders.include?(node.class) && node.name == name && node.location.start_offset > offset
120
+
121
+ node.compact_child_nodes.any? { |child| binds?(child, binders, name, offset) }
122
+ end
123
+
124
+ # Whether a write stores the key's value or something a call on it answers (`j = k`, `s = k.itself`,
125
+ # `s = prop.column_type.to_s`): its value is the key's root read, or a receiver chain that starts with the
126
+ # whole key chain. Such a copy may be the key object itself, and mutating it changes the key.
127
+ def aliases?(node, key)
128
+ return true if WRITE_NODES.include?(node.class) && derived_from_key?(node.value, key)
129
+
130
+ node.compact_child_nodes.any? { |child| aliases?(child, key) }
131
+ end
132
+
133
+ def derived_from_key?(value, key)
134
+ path = receiver_path(value)
135
+ !path.nil? && path.first(key.path.size) == key.path
136
+ end
137
+
138
+ # `[[root_kind, root_name], method, …]` for a local / ivar read or a receiver chain rooted at one, or nil.
139
+ def receiver_path(node)
140
+ names = []
141
+ while node.is_a?(Prism::CallNode) && node.receiver
142
+ names.unshift(node.name)
143
+ node = node.receiver
144
+ end
145
+ root = chain_root(node)
146
+ root && [root, *names]
147
+ end
148
+
149
+ # Whether a block or lambda body reads `ref` at all.
150
+ def closure_reads?(node, ref)
151
+ return reads?(node, ref) if CLOSURE_NODES.include?(node.class)
152
+
153
+ node.compact_child_nodes.any? { |child| closure_reads?(child, ref) }
154
+ end
155
+
156
+ def reads?(node, ref)
157
+ receiver_ref(node) == ref || node.compact_child_nodes.any? { |child| reads?(child, ref) }
158
+ end
159
+
160
+ # Whether the source between the guard and the read holds anything that may break the guard, read in source
161
+ # order so a later operand of the guard's own condition counts (`h.key?(k) && purge(h, k)`). A read before its
162
+ # guard (a loop back-edge) is taken to intervene.
163
+ def intervening?(walk, guard, read_node)
164
+ start = walk.guard_ends[guard]
165
+ stop = read_node.location.start_offset
166
+ return true if start.nil? || start > stop
167
+
168
+ body = walk.enclosing_def(read_node) || walk.root
169
+ breaks_in_region?(body, guard, start, stop)
170
+ end
171
+
172
+ def breaks_in_region?(node, guard, start, stop)
173
+ location = node.location
174
+ return false if location.end_offset <= start || location.start_offset >= stop
175
+
176
+ return true if location.start_offset >= start && location.end_offset <= stop && node_breaks?(node, guard)
177
+
178
+ node.compact_child_nodes.any? { |child| breaks_in_region?(child, guard, start, stop) }
179
+ end
180
+
181
+ def node_breaks?(node, guard)
182
+ (node.is_a?(Prism::CallNode) && breaks?(node, guard)) ||
183
+ (WRITE_NODES.include?(node.class) && aliases_guard?(node, guard)) ||
184
+ rebinds_guard?(node, guard) ||
185
+ (CLOSURE_NODES.include?(node.class) && closure_breaks?(node, guard))
186
+ end
187
+
188
+ def aliases_guard?(node, guard)
189
+ kind, name, key = guard
190
+ mentions?(node.value, [kind, name]) || mentions?(node.value, key.root)
191
+ end
192
+
193
+ def rebinds_guard?(node, guard)
194
+ kind, name, key = guard
195
+ [[kind, name], key.root].any? do |ref_kind, ref_name|
196
+ binders = { local: LOCAL_BINDERS, ivar: IVAR_BINDERS }[ref_kind]
197
+ binders&.include?(node.class) && node.name == ref_name
198
+ end
199
+ end
200
+
201
+ def closure_breaks?(node, guard)
202
+ kind, name, key = guard
203
+ reads?(node, [kind, name]) || reads?(node, key.root)
204
+ end
205
+
206
+ # Whether `call_node` may break the guard `[receiver_kind, receiver_name, key]`:
207
+ # - it passes the receiver as an argument (or `self`, when an instance variable is involved), or its block
208
+ # mentions the receiver or the key's root;
209
+ # - it is a call on the receiver other than a blockless read ({READ_ONLY_RECEIVER_CALLS});
210
+ # - it is rooted at the key's variable and does not re-read the key chain (`prop.reload`, `prop.x = 1`);
211
+ # - it is a call on `self` and the receiver or the key is an instance variable.
212
+ # Passing the key's root as an argument (`overridden?(prop)`) does not break it, as it does not end a
213
+ # method-chain narrowing.
214
+ def breaks?(call_node, guard)
215
+ kind, name, key = guard
216
+ receiver_ref = [kind, name]
217
+ return true if escapes_through_operands?(call_node, receiver_ref, key.root)
218
+
219
+ receiver = call_node.receiver
220
+ return kind == :ivar || key.root.first == :ivar if receiver.nil? || receiver.is_a?(Prism::SelfNode)
221
+ return true if receiver_ref(receiver) == receiver_ref && !read_only_call?(call_node)
222
+
223
+ touches_key_root?(call_node, key)
224
+ end
225
+
226
+ def escapes_through_operands?(call_node, receiver_ref, key_root)
227
+ args = call_node.arguments
228
+ return true if args && mentions?(args, receiver_ref)
229
+ # `other.mutate_owner(self)` hands on every instance variable.
230
+ return true if args && (receiver_ref.first == :ivar || key_root.first == :ivar) && mentions_self?(args)
231
+
232
+ block = call_node.block
233
+ return false if block.nil?
234
+
235
+ mentions?(block, receiver_ref) || (block.is_a?(Prism::BlockNode) && reads?(block, key_root))
236
+ end
237
+
238
+ def read_only_call?(call_node) = call_node.block.nil? && READ_ONLY_RECEIVER_CALLS.include?(call_node.name)
239
+
240
+ def touches_key_root?(call_node, key)
241
+ return false unless chain_root(call_node.receiver) == key.root
242
+
243
+ own = key_expr(call_node)
244
+ own.nil? || !key.prefixed_by?(own)
245
+ end
246
+
247
+ # The `[kind, name]` of the local / ivar a receiver chain (`a.b.c`, `a[0].b`) starts from, or nil.
248
+ def chain_root(node)
249
+ node = node.receiver while node.is_a?(Prism::CallNode) && node.receiver
250
+ case node
251
+ when Prism::LocalVariableReadNode then [:local, node.name]
252
+ when Prism::InstanceVariableReadNode then [:ivar, node.name]
253
+ end
254
+ end
255
+
256
+ def mentions_self?(node)
257
+ node.is_a?(Prism::SelfNode) || node.compact_child_nodes.any? { |child| mentions_self?(child) }
258
+ end
259
+
260
+ # Whether `node` holds a read of the variable `ref` in a position that may hand it on: anywhere but as the
261
+ # receiver of a blockless read ({READ_ONLY_RECEIVER_CALLS}), so `h[k]` and `MAP.key?(x)` do not count while
262
+ # `zap(h)`, `[h]`, `g = h` and `h.delete(k)` do.
263
+ def mentions?(node, ref)
264
+ return false if node.nil?
265
+
266
+ own = receiver_ref(node)
267
+ return own == ref if own
268
+
269
+ if node.is_a?(Prism::CallNode) && receiver_ref(node.receiver) == ref && read_only_call?(node)
270
+ return [node.arguments, node.block].any? { |part| mentions?(part, ref) }
271
+ end
272
+
273
+ node.compact_child_nodes.any? { |child| mentions?(child, ref) }
274
+ end
275
+
276
+ def receiver_ref(node) = KeyPresenceGuard.receiver_ref(node)
277
+ def key_expr(node) = KeyPresenceGuard.key_expr(node)
278
+ end
279
+ end
280
+ end
281
+ end