rigortype 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/docs/handbook/11-sig-gen.md +12 -5
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +6 -4
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +16 -9
- data/lib/rigor/analysis/check_rules/rule_ids.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +23 -22
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +89 -22
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +119 -15
- data/lib/rigor/analysis/reachability/scan.rb +332 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +26 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +28 -9
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner.rb +29 -11
- data/lib/rigor/analysis/worker_session.rb +1 -1
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +36 -8
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +33 -0
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/expression_typer.rb +58 -25
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer.rb +140 -5
- data/lib/rigor/inference/statement_evaluator.rb +223 -31
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/project_environment.rb +20 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +22 -11
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +18 -1
|
@@ -20,10 +20,18 @@ module Rigor
|
|
|
20
20
|
#
|
|
21
21
|
# Issue #1740 — {OwnMethod} shares {.levels} with the walk below and asks the opposite question for the two
|
|
22
22
|
# argument checks: does the receiver's own method answer ahead of every refinement in effect?
|
|
23
|
+
#
|
|
24
|
+
# Issue #1752 — a class object is walked through its own lookup ({ClassObject}): its singleton class and each
|
|
25
|
+
# superclass's, then `Class`, `Module`, `Object`, `Kernel` and `BasicObject`, so a refinement of `Object` answers
|
|
26
|
+
# `Integer.oo` while `Integer.sqrt` keeps answering ahead of one.
|
|
23
27
|
module RefinedDispatch
|
|
24
28
|
# The refinement that answers: `module_name` refines `refined_class` (a class or module on the walk).
|
|
25
29
|
Winner = Data.define(:module_name, :refined_class)
|
|
26
30
|
|
|
31
|
+
# Issue #1752 — the singleton class `#<Class:of>` as an entry of a class object's walk ({ClassObject}): it
|
|
32
|
+
# defines the name through `of`'s own singleton methods, and no refinement table key names it.
|
|
33
|
+
SingletonClass = Data.define(:of)
|
|
34
|
+
|
|
27
35
|
# The answer when the in-effect list carries {InEffectRefinements::UNKNOWN} and some refinement defines the
|
|
28
36
|
# name on a class the walk reaches: any of them may answer.
|
|
29
37
|
UNKNOWN = :unknown
|
|
@@ -37,14 +45,16 @@ module Rigor
|
|
|
37
45
|
def wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
38
46
|
|
|
39
47
|
# A {Winner}, {UNKNOWN}, or nil when no in-effect refinement answers `method_name` on an instance of
|
|
40
|
-
# `class_name` (the class's own lookup does)
|
|
48
|
+
# `class_name` (the class's own lookup does), or with `kind: :singleton` on the class object `class_name`
|
|
49
|
+
# itself (issue #1752). `list` is the call site's in-effect refinements.
|
|
41
50
|
#
|
|
42
51
|
# ADR-121 WD7 — a row the walk could not read never yields a {Winner}: an in-effect module's row for the name
|
|
43
52
|
# whose class the walk could not name reaches every receiver, and a names-wildcard row of a level the walk
|
|
44
53
|
# reaches before a definer may define the name, so both answer {UNKNOWN}. So does a winner whose module shares
|
|
45
54
|
# its last segment with another listed module (A1): which of a `using`'s declared candidates Ruby finds can
|
|
46
|
-
# depend on load order.
|
|
47
|
-
|
|
55
|
+
# depend on load order. A class object's walk always ends at `Object`, so where it cannot be read a row that
|
|
56
|
+
# may sit on it answers {UNKNOWN} too.
|
|
57
|
+
def winner(scope, class_name, method_name, list, kind: :instance)
|
|
48
58
|
refinements = scope.discovered_refinements
|
|
49
59
|
return UNKNOWN if class_unknown_row?(scope, refinements, method_name, list)
|
|
50
60
|
|
|
@@ -53,13 +63,26 @@ module Rigor
|
|
|
53
63
|
unknown = list.include?(InEffectRefinements::UNKNOWN)
|
|
54
64
|
return nil if targets.nil? && unread.nil? && !unknown
|
|
55
65
|
|
|
56
|
-
levels = levels(scope, class_name)
|
|
66
|
+
levels = levels(scope, class_name, kind: kind)
|
|
67
|
+
return unread_class_object(scope, refinements, method_name, list) if levels.nil? && kind == :singleton
|
|
57
68
|
return nil if levels.nil?
|
|
58
69
|
return UNKNOWN if unknown && unknown_reaches?(scope, refinements, method_name, levels)
|
|
59
70
|
|
|
60
71
|
walk_targets(scope, levels, method_name, [targets, unread], list)
|
|
61
72
|
end
|
|
62
73
|
|
|
74
|
+
# {UNKNOWN} when some row for `method_name` (or a names-wildcard row) that `list` puts in effect, or any row
|
|
75
|
+
# under the unknown marker, names a class or module a class object may reach ({ClassObject.reachable?}).
|
|
76
|
+
def unread_class_object(scope, refinements, method_name, list)
|
|
77
|
+
unknown = list.include?(InEffectRefinements::UNKNOWN)
|
|
78
|
+
reaches = refinements.any? do |refined, methods|
|
|
79
|
+
rows = [methods[method_name], methods[wildcard]].compact
|
|
80
|
+
!rows.empty? && (unknown || rows.any? { |modules| in_effect?(modules, list) }) &&
|
|
81
|
+
ClassObject.reachable?(scope, refined)
|
|
82
|
+
end
|
|
83
|
+
reaches ? UNKNOWN : nil
|
|
84
|
+
end
|
|
85
|
+
|
|
63
86
|
def walk_targets(scope, levels, method_name, (targets, unread), list)
|
|
64
87
|
return nil if targets.nil? && unread.nil?
|
|
65
88
|
|
|
@@ -165,20 +188,40 @@ module Rigor
|
|
|
165
188
|
# refinement first, as any level is; past that, the mixin may define the name, so a refinement further up the
|
|
166
189
|
# walk may or may not win and the answer is {UNKNOWN}, never the replaced method's. ADR-121 WD7: a level an
|
|
167
190
|
# in-effect module has a names-wildcard row for (`unread`) may define the name too, so it answers {UNKNOWN}.
|
|
191
|
+
#
|
|
192
|
+
# Issue #1752 — on a class object's walk a singleton level that only an entry no table names may answer
|
|
193
|
+
# (`:continue_unsure`) makes a refinement reached past it {UNKNOWN}: that entry may define the name.
|
|
168
194
|
def walk(scope, levels, method_name, targets, unread)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return targeted_after?(levels, index, targets, unread) ? UNKNOWN : nil if mixed
|
|
195
|
+
unsure = false
|
|
196
|
+
levels.each_index do |index|
|
|
197
|
+
answer = level_answer(scope, levels, index, method_name, [targets, unread])
|
|
198
|
+
next unsure = true if answer == :continue_unsure
|
|
199
|
+
next if answer == :continue
|
|
175
200
|
|
|
176
|
-
|
|
177
|
-
return answer unless answer == :continue
|
|
201
|
+
return unsure && answer.is_a?(Winner) ? UNKNOWN : answer
|
|
178
202
|
end
|
|
179
203
|
nil
|
|
180
204
|
end
|
|
181
205
|
|
|
206
|
+
def level_answer(scope, levels, index, method_name, (targets, unread))
|
|
207
|
+
level_class, entries, mixed, every = levels[index]
|
|
208
|
+
return UNKNOWN if level_class && unread.key?(level_class)
|
|
209
|
+
|
|
210
|
+
refining = level_class && targets[level_class]
|
|
211
|
+
return Winner.new(module_name: refining, refined_class: level_class) if refining
|
|
212
|
+
return targeted_after?(levels, index, targets, unread) ? UNKNOWN : nil if mixed
|
|
213
|
+
|
|
214
|
+
walk_level(scope, [entries, every], level_class, method_name, [targets, unread])
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def walk_level(scope, (entries, every), level_class, method_name, (targets, unread))
|
|
218
|
+
if level_class.is_a?(SingletonClass)
|
|
219
|
+
return ClassObject.walk_level(scope, every || entries, method_name, targets, unread)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
walk_entries(scope, entries, level_class, method_name, targets, unread)
|
|
223
|
+
end
|
|
224
|
+
|
|
182
225
|
# A level's prepended modules, its own method and its included modules, in that order: `:continue` when none
|
|
183
226
|
# answers, so the walk moves on.
|
|
184
227
|
def walk_entries(scope, entries, level_class, method_name, targets, unread)
|
|
@@ -205,7 +248,10 @@ module Rigor
|
|
|
205
248
|
# name. `every entry` (project levels only) is the level's entries in lookup order with each one that
|
|
206
249
|
# neither the project's resolver nor RBS can name kept as its candidate names (an Array) where `entries`
|
|
207
250
|
# leaves it out: the typer never reads it, and {OwnMethod} declines where a refinement may name one.
|
|
208
|
-
|
|
251
|
+
# `kind: :singleton` answers the class object `class_name` instead ({ClassObject.levels}, issue #1752).
|
|
252
|
+
def levels(scope, class_name, kind: :instance)
|
|
253
|
+
return ClassObject.levels(scope, class_name) if kind == :singleton
|
|
254
|
+
|
|
209
255
|
environment = scope.environment
|
|
210
256
|
return rbs_levels(scope, class_name) if environment&.class_known?(class_name)
|
|
211
257
|
|
|
@@ -257,8 +303,9 @@ module Rigor
|
|
|
257
303
|
|
|
258
304
|
# Does `name` define `method_name` on its instance side itself: a project `def` (its dependency edge recorded
|
|
259
305
|
# through `Scope#user_def_for`), another project definer (`attr_*`, `define_method`), or an RBS declaration
|
|
260
|
-
# written on `name` rather than on an ancestor?
|
|
306
|
+
# written on `name` rather than on an ancestor? A {SingletonClass} asks its own singleton methods.
|
|
261
307
|
def defines?(scope, name, method_name)
|
|
308
|
+
return ClassObject.defines?(scope, name.of, method_name) if name.is_a?(SingletonClass)
|
|
262
309
|
return true if scope.user_def_for(name, method_name)
|
|
263
310
|
return true if scope.discovered_method?(name, method_name, :instance)
|
|
264
311
|
|
|
@@ -269,4 +316,5 @@ module Rigor
|
|
|
269
316
|
end
|
|
270
317
|
end
|
|
271
318
|
|
|
319
|
+
require_relative "refined_dispatch/class_object"
|
|
272
320
|
require_relative "refined_dispatch/own_method"
|
|
@@ -20,6 +20,8 @@ module Rigor
|
|
|
20
20
|
# `cache = {}; [1, 2].find { |e| (cache[:first] ||= e) == 2 }` answered its own `2`, where Ruby keeps the
|
|
21
21
|
# first iteration's `1`, folded `find` to `2` and reported `found == 2` always-truthy. The pass marks the
|
|
22
22
|
# sites this module returns (`Scope#with_repeated_or_writes`), and the reading is withheld at a marked site.
|
|
23
|
+
# `StatementEvaluator` types a repeating body from one entry scope too — its statement pass and every ADR-56
|
|
24
|
+
# write-back pass — and marks the generic pass's sites there (issue #1336).
|
|
23
25
|
#
|
|
24
26
|
# The mark is per site, not per receiver, because nothing about the receiver's type can say that its slot
|
|
25
27
|
# was filled: `{}` widens to `Hash[Dynamic[top], Dynamic[top]]`, a bare `Hash.new` is not widened at all, and
|
|
@@ -101,13 +101,17 @@ module Rigor
|
|
|
101
101
|
# bodies from their CONVERGED bindings so per-line probes
|
|
102
102
|
# (`rigor annotate`) reflect the post-writeback state, not the
|
|
103
103
|
# cap-N intermediate constants. Off for the check path.
|
|
104
|
+
# @param widen_clauses — check-path flag (issue #1231): a rescue or
|
|
105
|
+
# `ensure` clause records the variables the `begin` body writes as
|
|
106
|
+
# `Dynamic[top]`. Off for every other reader, so parameter inference
|
|
107
|
+
# and sig-gen observe the clause's plain entry.
|
|
104
108
|
# @return identity-comparing
|
|
105
109
|
# table whose default value is `default_scope`.
|
|
106
110
|
# The no-op re-anchoring table (#722 residue 2): shared so the overwhelmingly common
|
|
107
111
|
# nothing-to-re-anchor walk allocates none.
|
|
108
112
|
EMPTY_RENAMES = {}.freeze
|
|
109
113
|
|
|
110
|
-
def index(root, default_scope:, converged_loop_recording: false) # rubocop:disable Metrics/AbcSize
|
|
114
|
+
def index(root, default_scope:, converged_loop_recording: false, widen_clauses: false) # rubocop:disable Metrics/AbcSize
|
|
111
115
|
# Slice A-declarations. Build the declaration overrides first so every scope handed to the StatementEvaluator
|
|
112
116
|
# already carries the table; structural sharing through `Scope#with_local` / `#with_fact` / `#with_self_type`
|
|
113
117
|
# propagates it across every derived scope.
|
|
@@ -194,7 +198,8 @@ module Rigor
|
|
|
194
198
|
|
|
195
199
|
on_enter = ->(node, scope) { table[node] = scope }
|
|
196
200
|
StatementEvaluator.new(scope: seeded_scope, on_enter: on_enter,
|
|
197
|
-
converged_loop_recording: converged_loop_recording
|
|
201
|
+
converged_loop_recording: converged_loop_recording,
|
|
202
|
+
widen_clauses: widen_clauses).evaluate(root)
|
|
198
203
|
|
|
199
204
|
propagate(root, table, seeded_scope)
|
|
200
205
|
table
|
|
@@ -3990,6 +3995,7 @@ module Rigor
|
|
|
3990
3995
|
if methods_acc.mentions_refine? && methods_acc.refine_in?(location.start_offset, location.end_offset)
|
|
3991
3996
|
node.rigor_each_child { |child| census_scan(child, qualified_prefix, methods_acc) }
|
|
3992
3997
|
end
|
|
3998
|
+
record_foreign_singleton_def(node, qualified_prefix, owner_prefix, methods_acc.at(node))
|
|
3993
3999
|
unless def_owner_prefix&.empty? || defs_singleton == :unnameable ||
|
|
3994
4000
|
(singleton_cref && owner_prefix.empty?)
|
|
3995
4001
|
singleton_def = in_singleton_class || defs_singleton
|
|
@@ -4729,6 +4735,50 @@ module Rigor
|
|
|
4729
4735
|
acc[:deferred_ranges][path] = build_deferred_ranges(root)
|
|
4730
4736
|
record_class_sources(acc[:class_sources], path, root, superclasses, includes, file_def_nodes,
|
|
4731
4737
|
acc[:compact_headers])
|
|
4738
|
+
record_singleton_class_sources(acc[:singleton_class_sources], path, root)
|
|
4739
|
+
end
|
|
4740
|
+
|
|
4741
|
+
# `{name => Set[path]}` for a bundle's name list (a bundle written before the list existed has none).
|
|
4742
|
+
def bundle_path_sets(names, path) = (names || []).to_h { |name| [name, Set[path]] }
|
|
4743
|
+
|
|
4744
|
+
# Issue #1752 — the constants a `class << Const` body opens in this file, and those whose singleton side a call
|
|
4745
|
+
# there may rewrite ({#singleton_side_call?}). Such a body records facts about
|
|
4746
|
+
# `Const`'s singleton class (a `remove_method`, a `def`) without declaring `Const`, so it stays out of
|
|
4747
|
+
# `class_sources` (whose readers take a name there as one the project declares) and joins only the per-file
|
|
4748
|
+
# class-declaration sets the incremental `class:` edges diff (`Runner#class_declarations`).
|
|
4749
|
+
def record_singleton_class_sources(sources, path, root)
|
|
4750
|
+
collect_singleton_class_targets(root, []) { |name| (sources[name] ||= Set.new) << path }
|
|
4751
|
+
end
|
|
4752
|
+
|
|
4753
|
+
def collect_singleton_class_targets(node, lexical_prefix, &)
|
|
4754
|
+
case node
|
|
4755
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
4756
|
+
prefix = Source::ConstantPath.declaration_prefix(lexical_prefix, node.constant_path)
|
|
4757
|
+
node.body && prefix && collect_singleton_class_targets(node.body, prefix, &)
|
|
4758
|
+
return
|
|
4759
|
+
when Prism::SingletonClassNode
|
|
4760
|
+
rendered = singleton_receiver_constant_name(node.expression)
|
|
4761
|
+
yield eval_constant_receiver_prefix(node, node.expression, rendered, lexical_prefix).join("::") if rendered
|
|
4762
|
+
when Prism::CallNode
|
|
4763
|
+
yield_singleton_side_constant(node, node.receiver, lexical_prefix, &) if singleton_side_call?(node)
|
|
4764
|
+
end
|
|
4765
|
+
node.rigor_each_child { |child| collect_singleton_class_targets(child, lexical_prefix, &) }
|
|
4766
|
+
end
|
|
4767
|
+
|
|
4768
|
+
# A call on a constant that may rewrite its singleton side (`Const.singleton_class`,
|
|
4769
|
+
# `Const.define_singleton_method`, `Const.extend(M)`, `Const.instance_eval { … }`) records facts about `Const`'s
|
|
4770
|
+
# singleton methods the refinement walk reads from a file that need not declare `Const`, so the file joins the
|
|
4771
|
+
# constant's set too. Written inside `Const`'s own body, the name is in the file's set already; a `def Const.x`
|
|
4772
|
+
# files its own `method:` key.
|
|
4773
|
+
def singleton_side_call?(node)
|
|
4774
|
+
node.name == :singleton_class || surface_rewriting_call?(node)
|
|
4775
|
+
end
|
|
4776
|
+
|
|
4777
|
+
def yield_singleton_side_constant(node, receiver, lexical_prefix)
|
|
4778
|
+
return unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
4779
|
+
|
|
4780
|
+
rendered = singleton_receiver_constant_name(receiver)
|
|
4781
|
+
yield eval_constant_receiver_prefix(node, receiver, rendered, lexical_prefix).join("::") if rendered
|
|
4732
4782
|
end
|
|
4733
4783
|
|
|
4734
4784
|
def build_deferred_ranges(root)
|
|
@@ -7160,6 +7210,7 @@ module Rigor
|
|
|
7160
7210
|
return record_module_function(node, current_class, accumulator, in_singleton) if node.name == :module_function
|
|
7161
7211
|
|
|
7162
7212
|
taint_opaque_eval(node, current_class, accumulator)
|
|
7213
|
+
return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
|
|
7163
7214
|
|
|
7164
7215
|
kind, _arguments, via_send = mixin_call_view(node)
|
|
7165
7216
|
return if kind.nil?
|
|
@@ -7172,6 +7223,49 @@ module Rigor
|
|
|
7172
7223
|
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
7173
7224
|
end
|
|
7174
7225
|
|
|
7226
|
+
# Issue #1752 — a call that rewrites a singleton class reached through `singleton_class` (`singleton_class.
|
|
7227
|
+
# class_eval { def x }`, `singleton_class.send(:define_method, :x)`, `Const.singleton_class.prepend(M)`,
|
|
7228
|
+
# `Const.singleton_class.send(:include, M)`) puts methods or modules on that singleton class that no edge
|
|
7229
|
+
# records, so its singleton side is unpositioned: the owner is the enclosing class for a bare or `self.`
|
|
7230
|
+
# receiver, else the constant. True when it tainted.
|
|
7231
|
+
def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
|
|
7232
|
+
receiver = node.receiver
|
|
7233
|
+
return false unless singleton_class_of_receiver?(receiver) && surface_rewriting_call?(node)
|
|
7234
|
+
return false if singleton_class_named_call?(node)
|
|
7235
|
+
|
|
7236
|
+
owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current_class)
|
|
7237
|
+
return false if owner.nil?
|
|
7238
|
+
|
|
7239
|
+
accumulator.taint(owner, :extend)
|
|
7240
|
+
true
|
|
7241
|
+
end
|
|
7242
|
+
|
|
7243
|
+
# A bare, `self.` or constant receiver's `singleton_class` call.
|
|
7244
|
+
def singleton_class_of_receiver?(receiver)
|
|
7245
|
+
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil?
|
|
7246
|
+
end
|
|
7247
|
+
|
|
7248
|
+
# An `attr_*` on a singleton class (`singleton_class.attr_accessor :conf`) rewrites only the names it spells, so
|
|
7249
|
+
# it makes those names' singleton envelopes opaque ({#record_singleton_class_names}) and leaves the rest of the
|
|
7250
|
+
# singleton side readable. An `attr_*` with a name it does not spell as a literal (`attr_accessor(*KEYS)`, a
|
|
7251
|
+
# block's variable), and an `alias_method` or `define_method` there, still taint the side: the own-definer read
|
|
7252
|
+
# would otherwise answer the body Ruby replaced (`definer_resolution_witness_spec.rb`).
|
|
7253
|
+
def singleton_class_named_call?(node)
|
|
7254
|
+
arguments = node.arguments&.arguments || []
|
|
7255
|
+
ATTR_MACROS.include?(node.name) && !arguments.empty? &&
|
|
7256
|
+
arguments.all? { |argument| literal_method_name(argument) }
|
|
7257
|
+
end
|
|
7258
|
+
|
|
7259
|
+
def singleton_class_owner(node, object, qualified_prefix, current_class)
|
|
7260
|
+
return current_class if object.nil? || object.is_a?(Prism::SelfNode)
|
|
7261
|
+
|
|
7262
|
+
rendered = Source::ConstantPath.qualified_name_or_nil(object)
|
|
7263
|
+
return nil if rendered.nil?
|
|
7264
|
+
return rendered if Source::ConstantPath.rooted?(object) || qualified_prefix.empty?
|
|
7265
|
+
|
|
7266
|
+
eval_constant_receiver_prefix(node, object, rendered, qualified_prefix).join("::")
|
|
7267
|
+
end
|
|
7268
|
+
|
|
7175
7269
|
# The bare `module_function` toggle: the module extends itself. A direct statement only.
|
|
7176
7270
|
def record_module_function(node, current_class, accumulator, in_singleton)
|
|
7177
7271
|
return unless current_class && node.receiver.nil? && !in_singleton && ModuleFunctionState.extends_self?(node)
|
|
@@ -7758,6 +7852,42 @@ module Rigor
|
|
|
7758
7852
|
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
7759
7853
|
record_surface_mark(tables, name, Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
|
|
7760
7854
|
end
|
|
7855
|
+
elsif singleton_class_of_receiver?(receiver) && singleton_class_named_call?(node)
|
|
7856
|
+
record_singleton_class_names(node, receiver.receiver, qualified_prefix, tables)
|
|
7857
|
+
end
|
|
7858
|
+
end
|
|
7859
|
+
|
|
7860
|
+
# Issue #1752 — the names a {#singleton_class_named_call?} rewrites on its singleton class's owner, each
|
|
7861
|
+
# singleton envelope opaque: the evidence the refinement typer reads as a rewritten singleton name.
|
|
7862
|
+
def record_singleton_class_names(node, object, qualified_prefix, tables)
|
|
7863
|
+
current_class = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
|
|
7864
|
+
owner = singleton_class_owner(node, object, qualified_prefix, current_class)
|
|
7865
|
+
return if owner.nil?
|
|
7866
|
+
|
|
7867
|
+
(node.arguments&.arguments || []).each do |argument|
|
|
7868
|
+
method_name = literal_method_name(argument)
|
|
7869
|
+
next unless method_name
|
|
7870
|
+
|
|
7871
|
+
names = [method_name]
|
|
7872
|
+
names << :"#{method_name}=" if %i[attr_writer attr_accessor].include?(node.name)
|
|
7873
|
+
names.each do |name|
|
|
7874
|
+
record_envelope(tables.envelopes, owner, [:singleton, name], Source::ParameterEnvelope::OPAQUE)
|
|
7875
|
+
contest_envelope(tables, owner, [:singleton, name]) unless tables.certain
|
|
7876
|
+
end
|
|
7877
|
+
end
|
|
7878
|
+
end
|
|
7879
|
+
|
|
7880
|
+
# Issue #1752 — `def Const.x` whose receiver is not the lexical `self` (written at the top level or in another
|
|
7881
|
+
# class's body) defines `Const`'s singleton method, which no table files under `Const`: its singleton name's
|
|
7882
|
+
# envelope turns opaque, the evidence the refinement typer reads as a rewritten singleton name.
|
|
7883
|
+
def record_foreign_singleton_def(node, qualified_prefix, owner_prefix, tables)
|
|
7884
|
+
receiver = node.receiver
|
|
7885
|
+
return unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
7886
|
+
return if def_receiver_targets_lexical_self?(receiver, owner_prefix)
|
|
7887
|
+
|
|
7888
|
+
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
7889
|
+
record_envelope(tables.envelopes, name, [:singleton, node.name], Source::ParameterEnvelope::OPAQUE)
|
|
7890
|
+
contest_envelope(tables, name, [:singleton, node.name]) unless tables.certain
|
|
7761
7891
|
end
|
|
7762
7892
|
end
|
|
7763
7893
|
|
|
@@ -8804,6 +8934,9 @@ module Rigor
|
|
|
8804
8934
|
merge_header_nestings(acc[:header_nestings], file_index[:header_nestings] || {})
|
|
8805
8935
|
fold_mixin_lists(acc, file_index)
|
|
8806
8936
|
file_index[:class_sources].each { |cn, files| (acc[:class_sources][cn] ||= Set.new).merge(files) }
|
|
8937
|
+
(file_index[:singleton_class_sources] || {}).each do |cn, files|
|
|
8938
|
+
(acc[:singleton_class_sources][cn] ||= Set.new).merge(files)
|
|
8939
|
+
end
|
|
8807
8940
|
# Issue #722 residue 2 — a pre-#722 seed bundle carries no candidates; the SCHEMA bump makes such a
|
|
8808
8941
|
# blob a cold rebuild, but default so any in-flight fold stays total.
|
|
8809
8942
|
acc[:compact_headers].merge!(file_index[:compact_headers] || {})
|
|
@@ -8892,6 +9025,7 @@ module Rigor
|
|
|
8892
9025
|
# data, so the bundle stays Marshal-clean and a warm file joins the envelopes its cold walk records.
|
|
8893
9026
|
parameter_envelopes: file_index[:parameter_envelopes],
|
|
8894
9027
|
class_source_names: file_index[:class_sources].keys,
|
|
9028
|
+
singleton_class_source_names: (file_index[:singleton_class_sources] || {}).keys,
|
|
8895
9029
|
# Issue #722 residue 2 — plain data, so the bundle stays Marshal-clean and a warm incremental file
|
|
8896
9030
|
# re-anchors its compact headers the way a cold walk of it does.
|
|
8897
9031
|
compact_headers: file_index[:compact_headers],
|
|
@@ -8916,7 +9050,7 @@ module Rigor
|
|
|
8916
9050
|
# ADR-85 WD2 — reconstitutes a cached bundle into a single-file index {#fold_file_index} folds: the
|
|
8917
9051
|
# def-node triples become {DefHandle}s bound to this file's `path`, and the class-source names become a
|
|
8918
9052
|
# `{name => Set[path]}` table (the shape `accumulate_project_index` produces).
|
|
8919
|
-
def bundle_to_file_index(bundle, path) # rubocop:disable Metrics/
|
|
9053
|
+
def bundle_to_file_index(bundle, path) # rubocop:disable Metrics/AbcSize -- one read per bundle slot
|
|
8920
9054
|
{
|
|
8921
9055
|
def_nodes: bundle_defs_to_handles(bundle[:def_nodes], path),
|
|
8922
9056
|
singleton_def_nodes: bundle_defs_to_handles(bundle[:singleton_def_nodes], path),
|
|
@@ -8937,7 +9071,8 @@ module Rigor
|
|
|
8937
9071
|
method_visibilities: bundle[:method_visibilities],
|
|
8938
9072
|
methods: bundle[:methods],
|
|
8939
9073
|
parameter_envelopes: bundle[:parameter_envelopes] || {},
|
|
8940
|
-
class_sources: bundle[:class_source_names]
|
|
9074
|
+
class_sources: bundle_path_sets(bundle[:class_source_names], path),
|
|
9075
|
+
singleton_class_sources: bundle_path_sets(bundle[:singleton_class_source_names], path),
|
|
8941
9076
|
compact_headers: bundle[:compact_headers] || {},
|
|
8942
9077
|
# Issue #644 — a pre-#644 bundle carries no census; the SCHEMA bump makes such a blob a cold
|
|
8943
9078
|
# rebuild, but default so any in-flight fold stays total.
|
|
@@ -8987,7 +9122,7 @@ module Rigor
|
|
|
8987
9122
|
method_visibilities: {},
|
|
8988
9123
|
methods: {},
|
|
8989
9124
|
deferred_ranges: {},
|
|
8990
|
-
parameter_envelopes: {}, class_sources: {},
|
|
9125
|
+
parameter_envelopes: {}, class_sources: {}, singleton_class_sources: {},
|
|
8991
9126
|
# Issue #722 residue 2 — compact-header re-anchor candidates, adjudicated in {#finalize_def_index}.
|
|
8992
9127
|
compact_headers: {},
|
|
8993
9128
|
constant_writes: {},
|