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,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
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ class InEffectRefinements
6
+ # Issue #1673 — the answers that need the project as well as the file: a `using`'s include expansion, the
7
+ # refining modules of a name, and the typer's entry ({Scope#in_effect_refinements}). Each records the
8
+ # dependencies its answer reads (ADR-46), so a warm run answers as a cold one.
9
+ module ScopeReads
10
+ # The in-effect refinements at `node` as {Scope#in_effect_refinements} answers them: the file's list from
11
+ # the query `Inference::ScopeIndexer` stamps on the discovery index, each `using` expanded through
12
+ # {.activated_modules}, then `declared`. A scope no index stamped answers `declared` alone.
13
+ def for_node(scope, node, declared = EMPTY)
14
+ query = scope.discovery.in_effect_refinements
15
+ return EMPTY if declared.empty? && (query.nil? || query.empty?)
16
+ return query.for_node(node, declared) { |name| activated_modules(scope, name) } if query
17
+
18
+ declared.uniq.freeze
19
+ end
20
+
21
+ # ADR-121 WD7 — the in-effect list at `offset` of `query`'s file with no block source, each `using` expanded
22
+ # through {.activated_modules}: what the check rules read through `Analysis::CheckRules::LexicalMethodSites`,
23
+ # and what {.for_node} answers the typer for the same node with no declared module, so the two agree.
24
+ def lexical_list(scope, query, offset)
25
+ query.at(offset) { |name| activated_modules(scope, name) }
26
+ end
27
+
28
+ # Issue #1664 — the `def` a refine body gives `method_name` on `class_name` for the refining module
29
+ # `module_name`, or nil where none can be read: this file's own refine body first, else one in a file that
30
+ # declares the module (`Scope#discovered_class_sources`), parsed once a run. A gem refinement, a module a
31
+ # `Module.new` write names in another file, or an anonymous module answer nil, which the typed arm reads as
32
+ # an unreadable body (`Dynamic[top]`). Issue #1749 — so does a name {RefineDefs::UNDECIDED} in this file or in
33
+ # any file that declares the module (a `refine` that runs inside a method, or an eval on a constant that
34
+ # several names denote): which refinement answers depends on an order and a module the walk cannot decide, and
35
+ # it can replace a module-body `def` of the name wherever it is written, so no body of the name is read. The
36
+ # consumer's `refinement:<name>` edge covers an edit to that body (`Incremental.changed_refinement_names`).
37
+ def refinement_def(scope, module_name, class_name, method_name)
38
+ refinement_def_with_query(scope, module_name, class_name, method_name).first
39
+ end
40
+
41
+ # {.refinement_def} as `[def_node, query]`, where `query` is the other file's {InEffectRefinements} the body was
42
+ # found in, or nil for this file's own body (or none): the typed arm re-types a foreign body under that file's
43
+ # in-effect refinements.
44
+ def refinement_def_with_query(scope, module_name, class_name, method_name)
45
+ chosen = nil
46
+ each_declaring_query(scope, module_name) do |query, foreign|
47
+ found = query&.refinement_def(module_name, class_name, method_name)
48
+ return NO_DEF if found == RefineDefs::UNDECIDED
49
+ next if found.nil? || chosen
50
+ return NO_DEF if found == RefineDefs::BODILESS
51
+
52
+ chosen = [found, (query if foreign)]
53
+ end
54
+ chosen || NO_DEF
55
+ end
56
+
57
+ # This file's query, then one per file that declares `module_name`.
58
+ def each_declaring_query(scope, module_name)
59
+ yield scope.discovery.in_effect_refinements, false
60
+ (scope.discovered_class_sources[module_name] || EMPTY).each do |path|
61
+ yield DefNodeResolver.refinement_query(path), true
62
+ end
63
+ end
64
+
65
+ NO_DEF = [nil, nil].freeze
66
+ private_constant :NO_DEF
67
+
68
+ # Issue #1689 — {InEffectRefinements#class_body_refine?} on the query stamped for `scope`'s file; false where no
69
+ # index stamped one, which keeps the call's refine-body reading.
70
+ def class_body_refine?(scope, call_node)
71
+ query = scope.discovery.in_effect_refinements
72
+ !query.nil? && query.class_body_refine?(call_node)
73
+ end
74
+
75
+ # Issue #1671 — the modules whose refinements `using name` puts in effect, in activation order: the project
76
+ # modules on `name`'s instance-side `Scope::ResolutionChain` (what a module includes and prepends,
77
+ # transitively) in reverse ancestor order, each at its first position, so the module's own refinements
78
+ # follow its includes and a module it prepends follows it and wins. CRuby's `using_module_recursive` walks
79
+ # `RCLASS_SUPER` to its end before each link, and a prepended module sits above the module's origin. `name`
80
+ # itself goes last when the chain does not list it. Nil, read as "any module may be in effect", when the
81
+ # chain may hold a module it does not list: it was cut at its limit, or a module on it records a mixin the
82
+ # tables cannot name.
83
+ #
84
+ # ADR-121 WD7 — {UNDECLARED} when nothing declares `name` ({.declared_module?}), which drops it from a
85
+ # `using`'s candidates (A1). A chain entry the project does not declare is dropped when it is a core or
86
+ # stdlib module (the core and standard-library signatures the rbs gem ships declare no refinements); any
87
+ # other enters by its spelling, which {.opaque_module?} then rejects, so a module included from a gem or from
88
+ # a file outside the analysed paths may refine anything.
89
+ #
90
+ # ADR-46 — the answer reads include edges declared in other files, so it depends on every file that
91
+ # declares a module on the chain, and on the existence of each one's name: a new file reopening `name`, or
92
+ # declaring an included module the project did not declare before, re-checks the consumer through
93
+ # `class:<name>`.
94
+ def activated_modules(scope, name)
95
+ unless declared_module?(scope, name)
96
+ Analysis::DependencyRecorder.read_last_segment(:class, name) if Analysis::DependencyRecorder.active?
97
+ return UNDECLARED
98
+ end
99
+
100
+ chain = Scope::ResolutionChain.for(scope, name, :instance, :constants)
101
+ record_chain(scope, name, chain) if Analysis::DependencyRecorder.active?
102
+ return nil if chain.truncated? || chain.wildcard_mixin?
103
+
104
+ modules = chain.entries.reverse_each.filter_map { |entry| activated_entry(scope, entry) }.uniq
105
+ modules.include?(name) ? modules : modules << name
106
+ end
107
+
108
+ # ADR-121 WD7 (A4) — can Rigor not read every refinement of `name`, a module in an in-effect list? True when
109
+ # the project does not declare it (a gem's module, a module required from outside the analysed paths, or
110
+ # one only an `Object.const_set` makes), unless it is a core or stdlib module, which refines nothing; and
111
+ # true when a targets-wildcard row (`{"*" => {"*" => [...]}}`) lists it or the wildcard, which some unreadable
112
+ # `refine` wrote. Every reader that declines on an opaque module asks this, and nothing else. Gem modules
113
+ # stay opaque even when the gem source walker read them.
114
+ #
115
+ # ADR-46 — the answer moves when a file declares or stops declaring the module (`class:<last segment>`) or
116
+ # when a targets-wildcard row appears or vanishes (`refinement:*`).
117
+ def opaque_module?(scope, name)
118
+ if Analysis::DependencyRecorder.active?
119
+ Analysis::DependencyRecorder.read_last_segment(:class, name)
120
+ Analysis::DependencyRecorder.read_name(:refinement, WILDCARD)
121
+ end
122
+ unread = scope.discovered_refinements.dig(WILDCARD, WILDCARD)
123
+ return true if unread && (unread.include?(name) || unread.include?(WILDCARD))
124
+ return false if project_module?(scope, name)
125
+
126
+ !core_or_stdlib_module?(scope, name)
127
+ end
128
+
129
+ # Does `list` (an in-effect list, the unknown marker skipped) hold an {.opaque_module?}?
130
+ def opaque_in?(scope, list)
131
+ list.any? { |name| name != UNKNOWN && opaque_module?(scope, name) }
132
+ end
133
+
134
+ # ADR-121 WD7 (A1) — does the project, RBS or an opted-in gem declare `name`? A `using`'s lexical candidates
135
+ # that nothing declares are not the module Ruby's lookup finds, so they leave the list.
136
+ def declared_module?(scope, name)
137
+ return true if project_module?(scope, name)
138
+ return true if scope.environment&.class_known?(name)
139
+
140
+ gem_module?(scope, name)
141
+ end
142
+
143
+ # Issue #1120 — every module that refines `method_name` into `class_name` or one of its ancestors (a
144
+ # refinement of `Object` reaches a `String` receiver), or nil when none does. The table is empty on a
145
+ # project that refines nothing, which answers without a lookup.
146
+ #
147
+ # ADR-121 WD7 — a row the walk could not read counts too: a names-wildcard row of the class (the body may
148
+ # define any name), and a row whose class the walk could not name ({.class_wildcard_key?}), which reaches
149
+ # every receiver.
150
+ #
151
+ # ADR-46 — the answer is a function of every refinement of this name in the project, so the consumer
152
+ # depends on the name whichever way it answers; a refine body edited in another file must re-check it
153
+ # (`IncrementalSession#refinement_affected`). It also depends on the wildcard rows (`refinement:*`).
154
+ def refining_modules(scope, class_name, method_name)
155
+ record_refinement_names(method_name)
156
+ refinements = scope.discovered_refinements
157
+ return nil if refinements.empty?
158
+
159
+ modules = nil
160
+ refinements.each do |refined, methods|
161
+ names = methods[method_name]
162
+ unread = methods[WILDCARD]
163
+ next if names.nil? && unread.nil?
164
+ next unless refined_receiver_class?(scope, class_name, refined) || class_wildcard_key?(scope, refined)
165
+
166
+ modules = (modules || []).concat(names || EMPTY, unread || EMPTY)
167
+ end
168
+ modules
169
+ end
170
+
171
+ # ADR-121 WD7 — {.refining_modules} for a receiver the class rows do not reach (a class object): only the rows
172
+ # whose class the walk could not name ({.class_wildcard_key?}), for the name or as a names-wildcard.
173
+ def class_unknown_refining_modules(scope, method_name)
174
+ record_refinement_names(method_name)
175
+ modules = nil
176
+ scope.discovered_refinements.each do |refined, methods|
177
+ names = methods[method_name]
178
+ unread = methods[WILDCARD]
179
+ next if (names.nil? && unread.nil?) || !class_wildcard_key?(scope, refined)
180
+
181
+ modules = (modules || []).concat(names || EMPTY, unread || EMPTY)
182
+ end
183
+ modules
184
+ end
185
+
186
+ # ADR-121 WD7 (A5) — the name edges a consumer of the refinement table records: the method's, and the
187
+ # wildcard's, so a wildcard row appearing or vanishing in another file re-checks it.
188
+ def record_refinement_names(method_name)
189
+ return unless Analysis::DependencyRecorder.active?
190
+
191
+ Analysis::DependencyRecorder.read_name(:refinement, method_name)
192
+ Analysis::DependencyRecorder.read_name(:refinement, WILDCARD)
193
+ end
194
+
195
+ # ADR-121 WD7 — is the refined-class key `refined` a class the walk could not name: the wildcard, or (A6) a
196
+ # name no class declaration or RBS knows, of which some segment is a constant a write binds: a project
197
+ # constant write (`K = String; refine(K)`), by the last segment the project's census keys writes on, or one an
198
+ # opted-in gem's files make (`DependencySourceInference::Index#bound_constant?`, issue #1814), by the name the
199
+ # write binds, asked for each prefix of the key (`Shared::Target` for `module Shared; Target = String; end` in
200
+ # a gem; `Pag::Enc` for the gem's own `module Pag; Enc = ::Encoding; refine(Enc::Converter)`), so a gem
201
+ # constant that merely shares a core class's name (`Names::String`) moves no sibling key (`Ext::String`).
202
+ # Every segment counts, not only the last, so a prefix alias (`Enc = ::Encoding; refine(Enc::Converter)`,
203
+ # #1815) reads the same way. A key that is a core or declared class never moves, whatever a file writes under
204
+ # its name. A constant nothing binds (a gem's class with no RBS) keeps a normal row, which matches no receiver.
205
+ # Each segment is a `constant:` dependency, so a file that starts or stops binding any of them re-checks the
206
+ # consumer.
207
+ def class_wildcard_key?(scope, refined)
208
+ return true if refined == WILDCARD
209
+ return false if scope.discovered_classes.key?(refined) || scope.environment&.class_known?(refined)
210
+
211
+ segments = refined.split("::").reject(&:empty?)
212
+ if Analysis::DependencyRecorder.active?
213
+ Analysis::DependencyRecorder.read_last_segment(:class, refined)
214
+ segments.each { |segment| Analysis::DependencyRecorder.read_last_segment(:constant, segment) }
215
+ end
216
+ segments.any? { |segment| !scope.bound_constant_names(segment).empty? } || gem_bound_key?(scope, segments)
217
+ end
218
+
219
+ # The gem half of {.class_wildcard_key?}: does an opted-in gem bind some prefix of the key (issue #1814)? A
220
+ # leading `Object::` spells the top level (`Object::Shared::Target` is `Shared::Target`), so it is dropped.
221
+ def gem_bound_key?(scope, segments)
222
+ index = scope.environment&.dependency_source_index
223
+ return false if index.nil? || !index.binds_constants?
224
+
225
+ segments = segments.drop_while { |segment| segment == "Object" }
226
+ segments.each_index.any? { |i| index.bound_constant?(segments[0..i].join("::")) }
227
+ end
228
+
229
+ private
230
+
231
+ WILDCARD = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
232
+ private_constant :WILDCARD
233
+
234
+ def activated_entry(scope, entry)
235
+ return entry.name unless entry.external?
236
+
237
+ candidates = entry.candidates
238
+ return nil if candidates.any? { |candidate| core_or_stdlib_module?(scope, candidate) }
239
+
240
+ candidates.first || entry.raw
241
+ end
242
+
243
+ def project_module?(scope, name)
244
+ scope.discovered_classes.key?(name) || !(scope.discovered_class_sources[name] || EMPTY).empty?
245
+ end
246
+
247
+ def core_or_stdlib_module?(scope, name)
248
+ scope.environment&.rbs_loader&.core_or_stdlib_class?(name) || false
249
+ end
250
+
251
+ # A module an opted-in gem's source declares: one its catalogue files a method under, or one its refinement
252
+ # rows name as a refining module.
253
+ def gem_module?(scope, name)
254
+ return true if scope.environment&.dependency_source_index&.gem_for(name)
255
+
256
+ scope.discovered_refinements.each_value.any? do |methods|
257
+ methods.each_value.any? { |modules| modules.include?(name) }
258
+ end
259
+ end
260
+
261
+ def refined_receiver_class?(scope, class_name, refined)
262
+ return true if class_name == refined
263
+
264
+ environment = scope.environment
265
+ !environment.nil? && environment.class_ordering(class_name, refined) == :subclass
266
+ end
267
+
268
+ def record_chain(scope, name, chain)
269
+ chain.record(scope)
270
+ Analysis::DependencyRecorder.read_last_segment(:class, name)
271
+ chain.entries.each do |entry|
272
+ if entry.external?
273
+ entry.candidates.each { |candidate| Analysis::DependencyRecorder.read_last_segment(:class, candidate) }
274
+ else
275
+ Analysis::DependencyRecorder.read_missing(:class, entry.last_segment)
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../source/constant_path"
4
+
5
+ module Rigor
6
+ module Inference
7
+ class InEffectRefinements
8
+ # The one walk of a file that records its activations, its refine-body `def`s and the `def`s a refine body
9
+ # defines on its refined class ({InEffectRefinements} describes what each source contributes).
10
+ module Walk
11
+ private
12
+
13
+ # `Module.nesting` inside `node`'s body while the block runs, for the refine-body defs recorded there.
14
+ def within_nesting(node)
15
+ outer = @nesting
16
+ @nesting = Source::ConstantPath.pushed_nesting(outer, node.constant_path) || outer
17
+ yield
18
+ ensure
19
+ @nesting = outer
20
+ end
21
+
22
+ # A file whose text names neither `using` nor `refine` (`refined` among it) holds none of the activations the
23
+ # walk records, so the scan of the source string spares it the tree walk.
24
+ def mentions_refinements?
25
+ text = @root.send(:source).source
26
+ text.include?("using") || text.include?("refine")
27
+ end
28
+
29
+ # `body` is the `[start, end]` of the body a `using` here stays in effect to the end of; `context` is the
30
+ # `RefineSelf::Context` of the `self` a `refine` here runs on, worked out by `RefineCensus`'s functions for
31
+ # every body, method and block this walk enters. The census reads it back for each `refine`-shaped node
32
+ # ({InEffectRefinements#refine_context}), so the two walks charge the same module.
33
+ def walk(node, prefix, body, in_def, context)
34
+ return if walked_body?(node, prefix, body, in_def, context)
35
+
36
+ case node
37
+ when Prism::SymbolNode
38
+ record_refine_context(node, context) if RefineCensus.refine_symbol?(node)
39
+ when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
40
+ Prism::ConstantPathOrWriteNode
41
+ return if walked_meta_new_write?(node, prefix, body, in_def, context)
42
+ when Prism::LambdaNode, Prism::BlockNode
43
+ # A call's block walks under {#walk_block_call}; a `super` / `yield`-side block or a lambda may run
44
+ # under any `self`.
45
+ return walk_children(node, prefix, body, in_def, RefineSelf.unknown)
46
+ when Prism::CallNode
47
+ record_call(node, prefix, body, in_def, context)
48
+ return walk_block_call(node, prefix, body, in_def, context) if node.block.is_a?(Prism::BlockNode)
49
+ end
50
+
51
+ walk_children(node, prefix, body, in_def, context)
52
+ end
53
+
54
+ # A `class` / `module` / `class << …` body or a `def` body, each with a `self` of its own.
55
+ def walked_body?(node, prefix, body, in_def, context)
56
+ case node
57
+ when Prism::ClassNode, Prism::ModuleNode
58
+ # A superclass expression runs where the `class` keyword is, a parameter's default in the method's body.
59
+ walk(node.superclass, prefix, body, in_def, context) if node.is_a?(Prism::ClassNode) && node.superclass
60
+ inner = Source::ConstantPath.declaration_prefix(prefix, node.constant_path) || prefix
61
+ within_nesting(node) do
62
+ walk_children(node.body, inner, span_of(node), false, declaration_context(node, inner))
63
+ end
64
+ when Prism::SingletonClassNode
65
+ walk(node.expression, prefix, body, in_def, context)
66
+ walk_children(node.body, prefix, span_of(node), false,
67
+ RefineSelf.singleton_class_body(context, node.expression.is_a?(Prism::SelfNode)))
68
+ when Prism::DefNode
69
+ method_context = RefineSelf.def_body(context, node.receiver)
70
+ walk_children(node.parameters, prefix, body, true, method_context)
71
+ walk_children(node.body, prefix, body, true, method_context)
72
+ else
73
+ return false
74
+ end
75
+ true
76
+ end
77
+
78
+ def declaration_context(node, inner)
79
+ return RefineSelf.module_body(inner.empty? ? nil : inner.join("::")) if node.is_a?(Prism::ModuleNode)
80
+
81
+ superclass = node.superclass
82
+ module_superclass = (superclass.is_a?(Prism::ConstantReadNode) ||
83
+ (superclass.is_a?(Prism::ConstantPathNode) && superclass.parent.nil?)) &&
84
+ superclass.name == :Module
85
+ RefineSelf.class_body(module_superclass)
86
+ end
87
+
88
+ # A block-carrying call: its receiver and arguments walk where the call is, its block under
89
+ # `RefineSelf.block` (a `define_method` block is a method body, where `using` raises).
90
+ def walk_block_call(node, prefix, body, in_def, context)
91
+ walk(node.receiver, prefix, body, in_def, context) if node.receiver
92
+ walk_children(node.arguments, prefix, body, in_def, context)
93
+ eval_names = RefineSelf.eval_receiver_names(node, prefix, context)
94
+ block_context = RefineSelf.block(context, node, eval_names) do |receiver|
95
+ literal_bound?(receiver, prefix, node.block)
96
+ end
97
+ walk_children(node.block, prefix, body, in_def || RefineSelf.method_block?(node), block_context)
98
+ end
99
+
100
+ # Does every binding of the constant, local or instance variable `receiver` in this file give it a literal
101
+ # (`RefineSelf.literal_bindings`, worked out once per file)? Asked only for a `block` whose text names `refine`
102
+ # or opens a heredoc: no other block can hold a `refine`-shaped node, so its context is never read, and the
103
+ # unnamed module it then runs on is the declining answer anyway.
104
+ def literal_bound?(receiver, prefix, block)
105
+ return false unless refine_text_in?(block)
106
+
107
+ key = RefineSelf.binding_key(receiver, prefix)
108
+ !key.nil? && (@literal_bindings ||= RefineSelf.literal_bindings(@root)).include?(key)
109
+ end
110
+
111
+ # The census's cost gate (`MethodTables#refine_in?`) over `node`'s text: a `refine` word or a heredoc opener.
112
+ def refine_text_in?(node)
113
+ location = node.location
114
+ (@refine_offsets ||= [ScopeIndexer.refine_word_offsets(@root), ScopeIndexer.heredoc_offsets(@root)])
115
+ .any? { |offsets| ScopeIndexer.offset_in?(offsets, location.start_offset, location.end_offset) }
116
+ end
117
+
118
+ # ADR-121 WD7 — what the census reads back for a `refine`-shaped node.
119
+ def record_refine_context(node, context)
120
+ @refine_contexts[node.location.start_offset] = context
121
+ end
122
+
123
+ def walk_children(node, prefix, body, in_def, context)
124
+ return if node.nil?
125
+
126
+ node.rigor_each_child { |child| walk(child, prefix, body, in_def, context) }
127
+ end
128
+
129
+ # `M = Module.new do … end`: the block's `self` is the module the write names, so a `refine` in it refines
130
+ # for `M`. The write's other parts walk as they would anyway.
131
+ def walked_meta_new_write?(node, prefix, body, in_def, context)
132
+ call = ScopeIndexer.meta_new_block_call(node)
133
+ return false if call.nil? || !ScopeIndexer.module_new_call?(call)
134
+
135
+ named = meta_new_owner(node, prefix)
136
+ walk(call.receiver, prefix, body, in_def, context) if call.receiver
137
+ walk_children(call.arguments, prefix, body, in_def, context)
138
+ record_call(call, prefix, body, in_def, context)
139
+ walk_children(call.block, prefix, body, in_def, RefineSelf.module_body(named))
140
+ true
141
+ end
142
+
143
+ def meta_new_owner(node, prefix)
144
+ case node
145
+ when Prism::ConstantWriteNode, Prism::ConstantOrWriteNode then (prefix + [node.name.to_s]).join("::")
146
+ else Source::ConstantPath.declaration_prefix(prefix, node.target)&.join("::")
147
+ end
148
+ end
149
+
150
+ def record_call(node, prefix, body, in_def, context)
151
+ record_refine_string_contexts(node, context)
152
+ record_refine_context(node, context) if node.name == :refine
153
+ owners = RefineSelf.charged_modules(context)
154
+ owners = nil if owners.include?(RefineCensus.wildcard)
155
+ if context.here == RefineSelf::CLASS && ScopeIndexer.refine_call?(node)
156
+ record_class_body_refine(node)
157
+ elsif (target = ScopeIndexer.refine_target(node))
158
+ record_refine_block(node, owners, ScopeIndexer.constant_receiver_candidates(target, prefix), context)
159
+ elsif ScopeIndexer.refine_call?(node)
160
+ # ADR-121 WD7 — a target the walk cannot name (`refine(k)`): the block is still a refine body, in effect
161
+ # inside itself, with no class to file its defs under.
162
+ record_refine_block(node, owners, EMPTY, context)
163
+ elsif using_call?(node) && !in_def
164
+ record_using(node, prefix, body)
165
+ elsif node.name == :refined
166
+ record_refined_chain(node, prefix)
167
+ end
168
+ end
169
+
170
+ # By `order`; activations a `.refined` chain recorded share the literal's offset and keep their recording (call)
171
+ # order, so only then does the sort carry the index.
172
+ def sort_activations
173
+ if @chained_refined_calls.nil?
174
+ @activations.sort_by!(&:order)
175
+ else
176
+ @activations = @activations.sort_by.with_index { |activation, index| [activation.order, index] }
177
+ end
178
+ end
179
+
180
+ # ADR-121 WD1's `Proc#refined` source (#1666). The walk meets a chain's outermost `.refined` first, so it
181
+ # records the whole chain from the literal outwards — the call order — and marks the inner calls done.
182
+ def record_refined_chain(node, prefix)
183
+ return if @chained_refined_calls&.include?(node)
184
+
185
+ @chained_refined_calls ||= Set.new.compare_by_identity
186
+ literal, chain = ProcLiterals.refined_chain(node)
187
+ chain.each { |call| @chained_refined_calls << call }
188
+ return if literal.nil?
189
+
190
+ chain.each do |call|
191
+ (call.arguments&.arguments || EMPTY).each do |argument|
192
+ record_block_activation(literal, refined_argument_candidates(argument, prefix))
193
+ end
194
+ end
195
+ end
196
+
197
+ # A constant argument's lexical candidates, or nil ({UNKNOWN}) for any other argument: a local, a splat, a call.
198
+ def refined_argument_candidates(argument, prefix)
199
+ return nil unless argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode)
200
+
201
+ candidates = ScopeIndexer.constant_receiver_candidates(argument, prefix)
202
+ candidates.empty? ? nil : candidates
203
+ end
204
+
205
+ # An activation over a block's body (`block` a `Prism::BlockNode` or `Prism::LambdaNode`) that puts `names` in
206
+ # effect after the block site's lexical list, each expanded through its includes as a `using`'s is; nil names
207
+ # contribute {UNKNOWN}.
208
+ def record_block_activation(block, names)
209
+ start, stop = span_of(block)
210
+ @activations << Activation.new(order: start, start: start, stop: stop, names: names, expand: true,
211
+ refine_block: false)
212
+ end
213
+
214
+ # `owners` are the refining module's names (several where its spelling can denote several), or nil; `context`
215
+ # is the `RefineSelf::Context` the `refine` call runs in.
216
+ def record_refine_block(node, owners, targets, context)
217
+ start, stop = span_of(node.block)
218
+ @activations << Activation.new(order: start, start: start, stop: stop, names: owners, expand: false,
219
+ refine_block: true)
220
+ body = node.block.body
221
+ return if body.nil?
222
+
223
+ ScopeIndexer.each_refinement_def(body) { |def_node| @refinement_defs << def_node.location.start_offset }
224
+ return if owners.nil? || targets.empty?
225
+
226
+ # ADR-121 WD7 (issue #1799) — a `def`, and an alias of a `def` the body wrote earlier; a later definer with no
227
+ # body (`define_method`, `attr_*`, `undef`) clears the name, which then types as `Dynamic[top]`. A `refine`
228
+ # that runs inside a method (issue #1749) creates its refinement only when the method runs, after or before a
229
+ # `using` and over or under a module-body `def` of the name, an order no walk can decide: no body of its names
230
+ # is kept, and no later `def` of one replaces that ({RefineDefs#record}).
231
+ RefineCensus.read_body(body).defs.each do |name, def_node|
232
+ @refine_defs.record(owners, targets, name, def_node, @nesting, created: context.deferred)
233
+ end
234
+ end
235
+
236
+ def record_class_body_refine(node) = (@class_body_refines ||= Set.new) << node.block.location.start_offset
237
+
238
+ def record_refine_string_contexts(node, context)
239
+ return unless RefineCensus.string_literal_call?(node)
240
+
241
+ node.arguments&.arguments&.each do |argument|
242
+ record_refine_context(argument, context) if RefineCensus.refine_string?(argument)
243
+ end
244
+ end
245
+
246
+ def using_call?(node)
247
+ node.name == :using && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)) &&
248
+ node.arguments&.arguments&.size == 1
249
+ end
250
+
251
+ def record_using(node, prefix, body)
252
+ argument = node.arguments.arguments.first
253
+ order = node.location.end_offset
254
+ candidates =
255
+ if argument.is_a?(Prism::ConstantReadNode) || argument.is_a?(Prism::ConstantPathNode)
256
+ ScopeIndexer.constant_receiver_candidates(argument, prefix)
257
+ end
258
+ if candidates && !candidates.empty?
259
+ @activations << Activation.new(order: order, start: order, stop: body.last, names: candidates,
260
+ expand: true, refine_block: false)
261
+ else
262
+ @unresolved_using = true
263
+ location = @root.location
264
+ @activations << Activation.new(order: order, start: location.start_offset, stop: location.end_offset,
265
+ names: nil, expand: false, refine_block: false)
266
+ end
267
+ end
268
+
269
+ def span_of(node)
270
+ location = node.location
271
+ [location.start_offset, location.end_offset]
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end