rigortype 0.4.4 → 0.4.6
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 +5 -5
- data/docs/handbook/11-sig-gen.md +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +136 -7
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +1088 -213
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +59 -7
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
|
@@ -25,6 +25,7 @@ require_relative "hash_lookup_mutation"
|
|
|
25
25
|
require_relative "in_effect_refinements"
|
|
26
26
|
require_relative "refine_census"
|
|
27
27
|
require_relative "refine_self"
|
|
28
|
+
require_relative "reflective_ivar_write"
|
|
28
29
|
require_relative "index_write_widening"
|
|
29
30
|
require_relative "module_function_state"
|
|
30
31
|
require_relative "multi_target_binder"
|
|
@@ -42,6 +43,8 @@ require_relative "scope_indexer/layered_def_nestings"
|
|
|
42
43
|
require_relative "scope_indexer/mixin_accumulator"
|
|
43
44
|
require_relative "scope_indexer/certainty"
|
|
44
45
|
require_relative "scope_indexer/member_layouts_collector"
|
|
46
|
+
require_relative "scope_indexer/declared_ivar_types"
|
|
47
|
+
require_relative "scope_indexer/declared_param_writes"
|
|
45
48
|
|
|
46
49
|
module Rigor
|
|
47
50
|
module Inference
|
|
@@ -111,7 +114,17 @@ module Rigor
|
|
|
111
114
|
# nothing-to-re-anchor walk allocates none.
|
|
112
115
|
EMPTY_RENAMES = {}.freeze
|
|
113
116
|
|
|
114
|
-
|
|
117
|
+
# No path whose contributions are all `possible` (issue #2021), the cross-file walks' default.
|
|
118
|
+
EMPTY_PATH_SET = Set.new.freeze
|
|
119
|
+
|
|
120
|
+
# `declared_ivar_types:` — whether an RBS-declared ivar type replaces the census's join of the writes (#1406).
|
|
121
|
+
# `rigor sig-gen` passes false: it proposes what the code says, and reading a declaration back would make a row
|
|
122
|
+
# count as generated only because it is declared.
|
|
123
|
+
# `ivar_writer_scope:` — the scope whose project tables the #1154 declared-parameter credit reads to find the
|
|
124
|
+
# ivar writers outside this file ({DeclaredParamWrites}); defaults to `default_scope`. `rigor sig-gen` indexes
|
|
125
|
+
# each file under an unseeded scope, so it passes one carrying the project's tables.
|
|
126
|
+
def index(root, default_scope:, converged_loop_recording: false, widen_clauses: false, # rubocop:disable Metrics/AbcSize
|
|
127
|
+
declared_ivar_types: true, ivar_writer_scope: nil)
|
|
115
128
|
# Slice A-declarations. Build the declaration overrides first so every scope handed to the StatementEvaluator
|
|
116
129
|
# already carries the table; structural sharing through `Scope#with_local` / `#with_fact` / `#with_self_type`
|
|
117
130
|
# propagates it across every derived scope.
|
|
@@ -133,7 +146,9 @@ module Rigor
|
|
|
133
146
|
|
|
134
147
|
# Slice 7 phase 2. Pre-pass over every class/module body to collect the per-class ivar accumulator. Seeded after
|
|
135
148
|
# declared_types so the rvalue typer in the pre-pass can see declaration overrides.
|
|
136
|
-
class_ivars =
|
|
149
|
+
class_ivars = DeclaredParamWrites.with_writer_scope(ivar_writer_scope) do
|
|
150
|
+
build_class_ivar_index(root, seeded_scope, declared_ivar_types: declared_ivar_types)
|
|
151
|
+
end
|
|
137
152
|
seeded_scope = seeded_scope.with_discovery(seeded_scope.discovery.with(class_ivars: class_ivars))
|
|
138
153
|
|
|
139
154
|
# Slice 7 phase 6. Same pre-pass shape for cvars (per class) and globals (program-wide). Globals are also
|
|
@@ -196,15 +211,27 @@ module Rigor
|
|
|
196
211
|
# share.
|
|
197
212
|
seeded_scope = seed_struct_fold_safe(seeded_scope, root).with_match_frame(root)
|
|
198
213
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
converged_loop_recording: converged_loop_recording,
|
|
202
|
-
widen_clauses: widen_clauses).evaluate(root)
|
|
214
|
+
evaluate = recording_evaluator(table, converged_loop_recording, widen_clauses)
|
|
215
|
+
evaluate.call(root, seeded_scope)
|
|
203
216
|
|
|
204
|
-
propagate(root, table, seeded_scope)
|
|
217
|
+
with_unentered_evaluator(evaluate) { propagate(root, table, seeded_scope) }
|
|
205
218
|
table
|
|
206
219
|
end
|
|
207
220
|
|
|
221
|
+
# The `(node, scope, excluded = nil) ->` evaluation that records every node it visits into `table`, except the
|
|
222
|
+
# nodes in `excluded`: the file's own pass, and each construct {#evaluate_unentered_flow} evaluates in the walk
|
|
223
|
+
# after it.
|
|
224
|
+
def recording_evaluator(table, converged_loop_recording, widen_clauses)
|
|
225
|
+
lambda do |node, scope, excluded = nil|
|
|
226
|
+
on_enter =
|
|
227
|
+
if excluded.nil? || excluded.empty? then ->(visited, at) { table[visited] = at }
|
|
228
|
+
else ->(visited, at) { table[visited] = at unless excluded.include?(visited) }
|
|
229
|
+
end
|
|
230
|
+
StatementEvaluator.new(scope: scope, on_enter: on_enter, converged_loop_recording: converged_loop_recording,
|
|
231
|
+
widen_clauses: widen_clauses).evaluate(node)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
208
235
|
# Runs the combined methods/def-nodes descent (one walk of the file), seeds the discovered-methods existence table
|
|
209
236
|
# onto `seeded_scope` (merged UNDER the cross-file pre-pass seed `default_scope` carries), and returns `[scope,
|
|
210
237
|
# file_def_nodes, file_envelopes]` so the caller can thread the def-node and issue #992 envelope tables into
|
|
@@ -332,8 +359,10 @@ module Rigor
|
|
|
332
359
|
# possible methods were folded already ({#seed_discovered_methods}).
|
|
333
360
|
def per_file_siblings(siblings, file_def_nodes, file_siblings, file_singleton, file_visibilities)
|
|
334
361
|
file_singleton_def_nodes, file_singleton_contested = file_singleton
|
|
335
|
-
|
|
336
|
-
|
|
362
|
+
# The seed's singleton contests stay ({#fold_visibility_contests}); it already holds a disagreement between
|
|
363
|
+
# this file and another.
|
|
364
|
+
siblings[:contested_discovered_method_visibilities] = fold_visibility_contests(
|
|
365
|
+
siblings[:contested_discovered_method_visibilities], {}, *file_visibilities
|
|
337
366
|
)
|
|
338
367
|
siblings[:contested_discovered_def_nodes] = fold_slot_contests(
|
|
339
368
|
siblings[:contested_discovered_def_nodes], file_def_nodes, file_siblings[:contested_discovered_def_nodes]
|
|
@@ -495,7 +524,7 @@ module Rigor
|
|
|
495
524
|
# (singleton vs instance). The rvalue is typed with NO local bindings — the pre-pass lacks statement-level
|
|
496
525
|
# threading — so `@x = 1` records `Constant[1]` but `@x = some_local + 1` records `Dynamic[Top]` (since
|
|
497
526
|
# `some_local` is unbound at pre-pass time). Multiple writes to the same ivar union via `Type::Combinator.union`.
|
|
498
|
-
def build_class_ivar_index(root, default_scope)
|
|
527
|
+
def build_class_ivar_index(root, default_scope, declared_ivar_types: true)
|
|
499
528
|
accumulator = {}
|
|
500
529
|
mutated_ivars = {}
|
|
501
530
|
read_before_write = {}
|
|
@@ -507,20 +536,25 @@ module Rigor
|
|
|
507
536
|
method_assign_effects = build_method_assign_effects(root)
|
|
508
537
|
walk_class_ivars(root, [], default_scope, accumulator, mutated_ivars,
|
|
509
538
|
read_before_write, init_writes, method_assign_effects)
|
|
539
|
+
reflective_opaque = accumulator.delete(REFLECTIVE_OPAQUE_CLASSES)
|
|
510
540
|
exempt_class_body_writes!(init_writes, accumulator.delete(DEFINE_METHOD_WRITES) || {})
|
|
511
541
|
merge_held_ivar_writes!(accumulator)
|
|
512
542
|
record_aliased_ivar_mutations!(root, mutated_ivars)
|
|
513
543
|
widen_mutated_ivar_entries!(accumulator, mutated_ivars)
|
|
514
544
|
contribute_read_before_write_nil!(accumulator, read_before_write, init_writes)
|
|
545
|
+
make_reflective_classes_gradual!(accumulator, reflective_opaque)
|
|
546
|
+
# #1406 — an RBS `@x: T` or `attr_* x: T` declaration replaces the census's join of the writes.
|
|
547
|
+
DeclaredIvarTypes.apply!(accumulator, default_scope) if declared_ivar_types
|
|
515
548
|
accumulator.transform_values(&:freeze).freeze
|
|
516
549
|
end
|
|
517
550
|
|
|
518
551
|
# Folds the class-body writes {#collect_class_body_ivar_writes} held under {CLASS_BODY_WRITES} into the
|
|
519
|
-
# `init_writes` exemption, except for an ivar a `define_method` block
|
|
520
|
-
#
|
|
521
|
-
#
|
|
522
|
-
#
|
|
523
|
-
#
|
|
552
|
+
# `init_writes` exemption, except for an ivar a `define_method` block, or a `block_as_methods:` block bound to the
|
|
553
|
+
# receiver's instance ({#method_body_block_call?}), of the class writes (#1768). Such an ivar had no census entry
|
|
554
|
+
# before the block's writes were counted, so the exemption never applied to it; a class-body `@x = nil` writes
|
|
555
|
+
# the class object's ivar and says nothing about whether an instance's `@x` is set, and exempting it would seed
|
|
556
|
+
# `@x` non-nil from the block's write alone (`if @x` folding always-truthy on an instance whose `@x` is unset).
|
|
557
|
+
# It keeps the read-before-write `nil` instead, which only widens.
|
|
524
558
|
def exempt_class_body_writes!(init_writes, define_method_writes)
|
|
525
559
|
(init_writes.delete(CLASS_BODY_WRITES) || {}).each do |class_name, ivars|
|
|
526
560
|
kept = ivars - (define_method_writes[class_name] || EMPTY_GUARDED_IVARS)
|
|
@@ -867,7 +901,7 @@ module Rigor
|
|
|
867
901
|
|
|
868
902
|
owner = def_owner || qualified_prefix
|
|
869
903
|
collect_initializer_block_ivars(node, owner, default_scope, accumulator, mutated_ivars, init_writes)
|
|
870
|
-
record_attr_writer_ivars(node, owner, accumulator) if ATTR_WRITER_MACROS.include?(node
|
|
904
|
+
record_attr_writer_ivars(node, owner, accumulator) if ATTR_WRITER_MACROS.include?(attr_macro(node))
|
|
871
905
|
collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
|
|
872
906
|
false
|
|
873
907
|
else
|
|
@@ -1018,8 +1052,11 @@ module Rigor
|
|
|
1018
1052
|
# nodes to skip; soundness is post-domination at the top statement level, so dropping the nil never hides a real
|
|
1019
1053
|
# runtime-nil read.
|
|
1020
1054
|
dead_writes = dead_transient_nil_writes(def_node.body, class_name, method_assign_effects)
|
|
1055
|
+
# #1154 — an `@x = x` from a declared parameter records the parameter's declared type.
|
|
1056
|
+
declared = DeclaredParamWrites::EMPTY
|
|
1057
|
+
declared = DeclaredParamWrites.collect(def_node, class_name, body_scope) unless singleton
|
|
1021
1058
|
gather_ivar_writes(def_node.body, body_scope, class_name, accumulator,
|
|
1022
|
-
EMPTY_GUARDED_IVARS, mutated_ivars, dead_writes)
|
|
1059
|
+
EMPTY_GUARDED_IVARS, mutated_ivars, dead_writes, declared)
|
|
1023
1060
|
|
|
1024
1061
|
# B2.3 — collect per-method evidence for the read-before- write nil contribution. The accumulator-level decision
|
|
1025
1062
|
# ("is this ivar truly read-before-write across the class lifetime?") is finalised at
|
|
@@ -1090,14 +1127,19 @@ module Rigor
|
|
|
1090
1127
|
# `owner` is empty in a `class << self` body (the call defines a class method there) and in an
|
|
1091
1128
|
# anonymous factory body, and the block contributes nothing there. A `define_method` inside a `def`
|
|
1092
1129
|
# body is already reached by that body's walk ({#gather_ivar_writes} enters blocks).
|
|
1130
|
+
#
|
|
1131
|
+
# An ADR-16 `block_as_methods:` call whose entry binds the receiver's instance (`get "/" do … end` in a
|
|
1132
|
+
# `Sinatra::Base` subclass under rigor-sinatra) takes the same arm: the plugin declares that its block runs as an
|
|
1133
|
+
# instance method of the receiver class, and `MacroBlockSelfType` narrows the block's `self` to that instance.
|
|
1093
1134
|
def collect_define_method_block_ivars(node, owner, default_scope, accumulator, mutated_ivars)
|
|
1094
1135
|
return if owner.empty?
|
|
1095
|
-
|
|
1136
|
+
|
|
1137
|
+
class_name = owner.join("::")
|
|
1138
|
+
return unless method_body_block_call?(node, class_name, default_scope)
|
|
1096
1139
|
|
|
1097
1140
|
block = node.block
|
|
1098
1141
|
return unless block.body
|
|
1099
1142
|
|
|
1100
|
-
class_name = owner.join("::")
|
|
1101
1143
|
body_scope = census_body_scope(default_scope, owner, Type::Combinator.nominal_of(class_name))
|
|
1102
1144
|
gather_ivar_writes(block.body, body_scope, class_name, accumulator, EMPTY_GUARDED_IVARS, mutated_ivars)
|
|
1103
1145
|
written = (accumulator[DEFINE_METHOD_WRITES] ||= {})[class_name] ||= Set.new
|
|
@@ -1115,6 +1157,8 @@ module Rigor
|
|
|
1115
1157
|
Prism::InstanceVariableTargetNode
|
|
1116
1158
|
acc << node.name
|
|
1117
1159
|
end
|
|
1160
|
+
reflective = ReflectiveIvarWrite.literal_name(node)
|
|
1161
|
+
acc << reflective if reflective
|
|
1118
1162
|
node.rigor_each_child { |child| define_method_written_ivars(child, acc) }
|
|
1119
1163
|
end
|
|
1120
1164
|
|
|
@@ -1243,6 +1287,25 @@ module Rigor
|
|
|
1243
1287
|
DefineMethodBlockSelf.define_method_on_lexical_self?(node) && node.block.is_a?(Prism::BlockNode)
|
|
1244
1288
|
end
|
|
1245
1289
|
|
|
1290
|
+
# A call with a literal block on the implicit or `self` receiver whose block is an instance method body of
|
|
1291
|
+
# `class_name`, the class the walk is in (#1768): `define_method`, or a call an ADR-16 `block_as_methods:` entry
|
|
1292
|
+
# declares runs its block as an instance method of the receiver class ({MacroBlockSelfType}). The entry is
|
|
1293
|
+
# matched on `Singleton[class_name]`, the receiver such a call has in the class body, through the same
|
|
1294
|
+
# receiver-ancestry match the evaluator narrows the block's `self` with, so the census counts exactly the blocks
|
|
1295
|
+
# whose `self` is narrowed to the instance.
|
|
1296
|
+
#
|
|
1297
|
+
# The class-body walk ({#collect_class_body_ivar_writes}) still records a `block_as_methods:` block's writes as
|
|
1298
|
+
# class-body writes, and {#exempt_class_body_writes!} takes back every ivar such a block writes.
|
|
1299
|
+
def method_body_block_call?(node, class_name, scope)
|
|
1300
|
+
return true if define_method_block_call?(node)
|
|
1301
|
+
return false unless node.block.is_a?(Prism::BlockNode)
|
|
1302
|
+
return false unless scope && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
1303
|
+
|
|
1304
|
+
MacroBlockSelfType.runs_as_receiver_instance_method?(
|
|
1305
|
+
scope: scope, call_node: node, receiver_type: Type::Combinator.singleton_of(class_name)
|
|
1306
|
+
)
|
|
1307
|
+
end
|
|
1308
|
+
|
|
1246
1309
|
def detect_read_before_write(node, seen_writes, read_first)
|
|
1247
1310
|
return unless node.is_a?(Prism::Node)
|
|
1248
1311
|
return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
@@ -1270,6 +1333,9 @@ module Rigor
|
|
|
1270
1333
|
end
|
|
1271
1334
|
|
|
1272
1335
|
seen_writes << node.name if IVAR_WRITE_NODES.any? { |klass| node.is_a?(klass) }
|
|
1336
|
+
# #1931 — a literal `instance_variable_set` is a write too; its value argument, read above, runs first.
|
|
1337
|
+
reflective = ReflectiveIvarWrite.literal_name(node)
|
|
1338
|
+
seen_writes << reflective if reflective
|
|
1273
1339
|
end
|
|
1274
1340
|
|
|
1275
1341
|
# Records each ivar target of a `MultiWriteNode` / nested `MultiTargetNode` into `seen_writes`, and descends into
|
|
@@ -1306,17 +1372,18 @@ module Rigor
|
|
|
1306
1372
|
private_constant :TAIL_VALUE_DEPTH_CAP
|
|
1307
1373
|
private_constant :EMPTY_GUARDED_IVARS
|
|
1308
1374
|
|
|
1309
|
-
def gather_ivar_writes(node, scope, class_name, accumulator, guarded_ivars = EMPTY_GUARDED_IVARS,
|
|
1310
|
-
mutated_ivars = nil, dead_writes = nil)
|
|
1375
|
+
def gather_ivar_writes(node, scope, class_name, accumulator, guarded_ivars = EMPTY_GUARDED_IVARS, # rubocop:disable Metrics/ParameterLists
|
|
1376
|
+
mutated_ivars = nil, dead_writes = nil, declared = DeclaredParamWrites::EMPTY)
|
|
1311
1377
|
return unless node.is_a?(Prism::Node)
|
|
1312
1378
|
|
|
1313
1379
|
if node.is_a?(Prism::InstanceVariableWriteNode)
|
|
1314
1380
|
unless dead_writes&.include?(node.object_id)
|
|
1315
1381
|
record_ivar_write(node, scope, class_name, accumulator,
|
|
1316
|
-
guarded: guarded_ivars.include?(node.name))
|
|
1382
|
+
guarded: guarded_ivars.include?(node.name), declared: declared[node])
|
|
1317
1383
|
end
|
|
1318
1384
|
else
|
|
1319
1385
|
record_compound_ivar_write(node, scope, class_name, accumulator)
|
|
1386
|
+
record_reflective_ivar_write(node, scope, class_name, accumulator)
|
|
1320
1387
|
end
|
|
1321
1388
|
|
|
1322
1389
|
# N1 — parallel / multiple assignment (`old, @cb = @cb, block`, `@i, @o, @e, @thr = Open3.popen3(cmd)`). A
|
|
@@ -1335,12 +1402,38 @@ module Rigor
|
|
|
1335
1402
|
|
|
1336
1403
|
if node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
|
|
1337
1404
|
walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars,
|
|
1338
|
-
mutated_ivars, dead_writes)
|
|
1405
|
+
mutated_ivars, dead_writes, declared)
|
|
1339
1406
|
return
|
|
1340
1407
|
end
|
|
1341
1408
|
|
|
1342
1409
|
node.rigor_each_child do |c|
|
|
1343
|
-
gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars, dead_writes)
|
|
1410
|
+
gather_ivar_writes(c, scope, class_name, accumulator, guarded_ivars, mutated_ivars, dead_writes, declared)
|
|
1411
|
+
end
|
|
1412
|
+
end
|
|
1413
|
+
|
|
1414
|
+
# #1931 — `instance_variable_set` on `self` writes an ivar no write node shows ({ReflectiveIvarWrite}). A literal
|
|
1415
|
+
# name (`instance_variable_set(:@k, v)`) contributes the value's type to that ivar's entry, as `@k = v` would. A
|
|
1416
|
+
# name the analysis cannot read (`instance_variable_set(:"@#{k}", v)`, the options-hash constructor idiom) may
|
|
1417
|
+
# write any ivar of the class, so every entry of the class turns gradual once the walk ends
|
|
1418
|
+
# ({#make_reflective_classes_gradual!}): a reader in another method otherwise kept a write's literal (`@timeout =
|
|
1419
|
+
# 5` read as `5`) and reported a call on it.
|
|
1420
|
+
def record_reflective_ivar_write(node, scope, class_name, accumulator)
|
|
1421
|
+
return unless ReflectiveIvarWrite.self_write?(node)
|
|
1422
|
+
|
|
1423
|
+
name = ReflectiveIvarWrite.literal_name(node)
|
|
1424
|
+
if name
|
|
1425
|
+
accumulate_ivar_type(accumulator, class_name, name, scope.type_of(ReflectiveIvarWrite.value_node(node)))
|
|
1426
|
+
else
|
|
1427
|
+
(accumulator[REFLECTIVE_OPAQUE_CLASSES] ||= Set.new) << class_name
|
|
1428
|
+
end
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
def make_reflective_classes_gradual!(accumulator, classes)
|
|
1432
|
+
classes&.each do |class_name|
|
|
1433
|
+
entries = accumulator[class_name]
|
|
1434
|
+
next if entries.nil?
|
|
1435
|
+
|
|
1436
|
+
accumulator[class_name] = entries.transform_values { Type::Combinator.untyped }
|
|
1344
1437
|
end
|
|
1345
1438
|
end
|
|
1346
1439
|
|
|
@@ -1390,22 +1483,22 @@ module Rigor
|
|
|
1390
1483
|
# init; end` would otherwise be marked too — but that pattern (write @x in the else of `if @x`) is a separate
|
|
1391
1484
|
# idiom whose surrounding reads of `@x` would then surface a nil-receiver FP. The ELSE branch is left ungarded so
|
|
1392
1485
|
# those reads continue to type as they did before this fix.
|
|
1393
|
-
def walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars,
|
|
1394
|
-
mutated_ivars = nil, dead_writes = nil)
|
|
1486
|
+
def walk_conditional_ivar_writes(node, scope, class_name, accumulator, guarded_ivars, # rubocop:disable Metrics/ParameterLists
|
|
1487
|
+
mutated_ivars = nil, dead_writes = nil, declared = DeclaredParamWrites::EMPTY)
|
|
1395
1488
|
then_guards = then_body_guarded_ivars(node)
|
|
1396
1489
|
then_guarded = then_guards.empty? ? guarded_ivars : (guarded_ivars | then_guards)
|
|
1397
1490
|
|
|
1398
1491
|
gather_ivar_writes(node.predicate, scope, class_name, accumulator, guarded_ivars,
|
|
1399
|
-
mutated_ivars, dead_writes)
|
|
1492
|
+
mutated_ivars, dead_writes, declared)
|
|
1400
1493
|
if node.statements
|
|
1401
1494
|
gather_ivar_writes(node.statements, scope, class_name, accumulator, then_guarded,
|
|
1402
|
-
mutated_ivars, dead_writes)
|
|
1495
|
+
mutated_ivars, dead_writes, declared)
|
|
1403
1496
|
end
|
|
1404
1497
|
branch = node.is_a?(Prism::IfNode) ? node.subsequent : node.else_clause
|
|
1405
1498
|
return unless branch
|
|
1406
1499
|
|
|
1407
1500
|
gather_ivar_writes(branch, scope, class_name, accumulator, guarded_ivars,
|
|
1408
|
-
mutated_ivars, dead_writes)
|
|
1501
|
+
mutated_ivars, dead_writes, declared)
|
|
1409
1502
|
end
|
|
1410
1503
|
|
|
1411
1504
|
# Returns the set of ivar names that, in the THEN body of this conditional, are statically known to be in a nil /
|
|
@@ -1882,8 +1975,8 @@ module Rigor
|
|
|
1882
1975
|
assigns.include?(target)
|
|
1883
1976
|
end
|
|
1884
1977
|
|
|
1885
|
-
def record_ivar_write(node, scope, class_name, accumulator, guarded: false)
|
|
1886
|
-
rvalue_type = scope.type_of(node.value)
|
|
1978
|
+
def record_ivar_write(node, scope, class_name, accumulator, guarded: false, declared: nil)
|
|
1979
|
+
rvalue_type = declared || scope.type_of(node.value)
|
|
1887
1980
|
|
|
1888
1981
|
# `@x = nil unless @x` / `@y = false unless @y` — follow-up to the polarity-aware defensive-init guard fix
|
|
1889
1982
|
# (ROADMAP § Future cycles — "Defensive ivar-init with nil / false rvalue"). When the rvalue is itself a falsey
|
|
@@ -2087,7 +2180,9 @@ module Rigor
|
|
|
2087
2180
|
# accumulator), and the class-body writes held out of `init_writes` until {#exempt_class_body_writes!}.
|
|
2088
2181
|
DEFINE_METHOD_WRITES = :define_method_writes
|
|
2089
2182
|
CLASS_BODY_WRITES = :class_body_writes
|
|
2090
|
-
|
|
2183
|
+
# The classes a computed-name `instance_variable_set` may write any ivar of ({#record_reflective_ivar_write}).
|
|
2184
|
+
REFLECTIVE_OPAQUE_CLASSES = :reflective_opaque_classes
|
|
2185
|
+
private_constant :DEFINE_METHOD_WRITES, :CLASS_BODY_WRITES, :REFLECTIVE_OPAQUE_CLASSES
|
|
2091
2186
|
|
|
2092
2187
|
# Unions `type` into the class-ivar accumulator for `(class_name, ivar_name)`. Shared by the single-write and
|
|
2093
2188
|
# multi-write (parallel-assignment) collectors.
|
|
@@ -3369,7 +3464,17 @@ module Rigor
|
|
|
3369
3464
|
# walk worked out for it ({#refine_context}).
|
|
3370
3465
|
MethodTables = Struct.new(:existence, :envelopes, :refinements, :certainty, :certain, :possible,
|
|
3371
3466
|
:contested_envelopes, :contested_def_nodes, :root, :in_effect, :census,
|
|
3372
|
-
:refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans
|
|
3467
|
+
:refine_text, :literal_seen, :source_bytes, :loose_constants, :block_spans,
|
|
3468
|
+
:definer_history) do
|
|
3469
|
+
# Issue #2006 — records, in walk order, that a definer other than an alias wrote the instance method `name`
|
|
3470
|
+
# of `class_name` at `node`: `def_node` for a `def`, nil for a `define_method(:x)` or an `attr_*` reader or
|
|
3471
|
+
# writer, and whether the write is {#certain}. {ScopeIndexer#apply_alias_def_nodes} reads the history to copy
|
|
3472
|
+
# what an alias's old name held at the alias, and to tell an alias a later definer replaced.
|
|
3473
|
+
def wrote_definer(class_name, name, node, def_node)
|
|
3474
|
+
entry = [node.location.start_offset, def_node, certain ? true : false].freeze
|
|
3475
|
+
((self.definer_history ||= {})[[class_name, name]] ||= []) << entry
|
|
3476
|
+
end
|
|
3477
|
+
|
|
3373
3478
|
# {ScopeIndexer.module_refine_target} against this tree's {InEffectRefinements}.
|
|
3374
3479
|
def refine_target_of(node)
|
|
3375
3480
|
return nil if ScopeIndexer.refine_target(node).nil?
|
|
@@ -4088,6 +4193,10 @@ module Rigor
|
|
|
4088
4193
|
(singleton_cref && owner_prefix.empty?)
|
|
4089
4194
|
record_alias_or_undef(node, owner_prefix, in_singleton_class || defs_singleton,
|
|
4090
4195
|
methods_acc.at(node))
|
|
4196
|
+
if node.is_a?(Prism::AliasMethodNode) && !(in_singleton_class || defs_singleton)
|
|
4197
|
+
supersede_def_nodes(Array(Source::AliasNames.keyword_names(node)&.first), owner_prefix,
|
|
4198
|
+
def_nodes_acc, methods_acc.at(node))
|
|
4199
|
+
end
|
|
4091
4200
|
end
|
|
4092
4201
|
return
|
|
4093
4202
|
when Prism::SymbolNode
|
|
@@ -4102,6 +4211,11 @@ module Rigor
|
|
|
4102
4211
|
end
|
|
4103
4212
|
anonymous = record_call_node_methods(node, owner_prefix, in_singleton_class, methods_acc.at(node),
|
|
4104
4213
|
source_path)
|
|
4214
|
+
unless in_singleton_class
|
|
4215
|
+
superseded = call_superseded_names(node)
|
|
4216
|
+
supersede_def_nodes(superseded, owner_prefix, def_nodes_acc, methods_acc.at(node))
|
|
4217
|
+
note_call_definers(node, superseded, owner_prefix, methods_acc.at(node))
|
|
4218
|
+
end
|
|
4105
4219
|
tables = methods_acc.at(node)
|
|
4106
4220
|
record_singleton_prepend(node, qualified_prefix, tables,
|
|
4107
4221
|
self_ok: node.name == :prepend && tables.in_block?(node))
|
|
@@ -4171,11 +4285,40 @@ module Rigor
|
|
|
4171
4285
|
def record_declaration_facts(node, child_prefix, tables)
|
|
4172
4286
|
if node.is_a?(Prism::ClassNode)
|
|
4173
4287
|
record_meta_superclass_members(node, child_prefix, tables)
|
|
4288
|
+
record_constructed_superclass(node.superclass, child_prefix.join("::"), tables)
|
|
4174
4289
|
else
|
|
4175
4290
|
record_surface_mark(tables, child_prefix.join("::"), Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK)
|
|
4176
4291
|
end
|
|
4177
4292
|
end
|
|
4178
4293
|
|
|
4294
|
+
# Issue #1979 — `class K < Data.define(:a)`, `< Struct.new(:a)` or `< Class.new(Parent)`: the superclass table
|
|
4295
|
+
# records `nil` for K, so a subclass walk enumerates it under every class. Each constant the expression names
|
|
4296
|
+
# (the factory's receiver, and `Class.new`'s argument, or `Object` for none) takes a
|
|
4297
|
+
# `[ENVELOPE_CONSTRUCTED_KIND, name]` entry; an expression of any other shape records none.
|
|
4298
|
+
def record_constructed_superclass(superclass, class_name, tables)
|
|
4299
|
+
names = constructed_superclass_names(superclass)
|
|
4300
|
+
names&.each do |name|
|
|
4301
|
+
record_envelope(tables.envelopes, class_name, [Scope::DiscoveryIndex::ENVELOPE_CONSTRUCTED_KIND, name],
|
|
4302
|
+
Source::ParameterEnvelope::OPAQUE)
|
|
4303
|
+
end
|
|
4304
|
+
end
|
|
4305
|
+
|
|
4306
|
+
def constructed_superclass_names(node)
|
|
4307
|
+
return ["Data"] if meta_call_with_name?(node, :Data, :define)
|
|
4308
|
+
return ["Struct"] if meta_call_with_name?(node, :Struct, :new)
|
|
4309
|
+
return nil unless class_new_call?(node)
|
|
4310
|
+
|
|
4311
|
+
arguments = node.arguments&.arguments || []
|
|
4312
|
+
return %w[Class Object] if arguments.empty?
|
|
4313
|
+
return nil unless arguments.one?
|
|
4314
|
+
|
|
4315
|
+
argument = arguments.first
|
|
4316
|
+
inner = if argument.is_a?(Prism::ConstantReadNode) then [argument.name.to_s]
|
|
4317
|
+
else constructed_superclass_names(argument)
|
|
4318
|
+
end
|
|
4319
|
+
inner && ["Class", *inner]
|
|
4320
|
+
end
|
|
4321
|
+
|
|
4179
4322
|
# The block form of a meta-new constant write: its members, and the module mark `Module.new` earns.
|
|
4180
4323
|
# Issue #1820 — `K = Class.new(Parent)`, with or without a block, names a class whose superclass the superclass
|
|
4181
4324
|
# table leaves out; it takes {Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK}.
|
|
@@ -4214,7 +4357,7 @@ module Rigor
|
|
|
4214
4357
|
# descent.
|
|
4215
4358
|
def record_call_node_methods(node, qualified_prefix, in_singleton_class, methods_acc, source_path)
|
|
4216
4359
|
record_define_method(node, qualified_prefix, in_singleton_class, methods_acc) if node.name == :define_method
|
|
4217
|
-
record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc) if
|
|
4360
|
+
record_attr_methods(node, qualified_prefix, in_singleton_class, methods_acc) if attr_macro(node)
|
|
4218
4361
|
record_module_attr_methods(node, qualified_prefix, methods_acc) if MODULE_ATTR_MACROS.key?(node.name)
|
|
4219
4362
|
record_alias_method_call(node, qualified_prefix, in_singleton_class, methods_acc)
|
|
4220
4363
|
record_surface_evidence(node, qualified_prefix, methods_acc)
|
|
@@ -4711,6 +4854,7 @@ module Rigor
|
|
|
4711
4854
|
accumulator[class_name] ||= {}
|
|
4712
4855
|
accumulator[class_name][def_node.name] = def_node
|
|
4713
4856
|
follow_slot_writer(tables, :contested_def_nodes, [class_name, def_node.name], tables.nil? || tables.certain)
|
|
4857
|
+
tables&.wrote_definer(class_name, def_node.name, def_node, def_node)
|
|
4714
4858
|
record_anonymous_body_def_as_toplevel(def_node, qualified_prefix, accumulator, tables)
|
|
4715
4859
|
end
|
|
4716
4860
|
|
|
@@ -4727,6 +4871,59 @@ module Rigor
|
|
|
4727
4871
|
end
|
|
4728
4872
|
end
|
|
4729
4873
|
|
|
4874
|
+
# Issue #1989 — the instance method names a non-`def` definer written on the class body's self replaces:
|
|
4875
|
+
# each reader (and `x=` writer) an `attr_*` call declares, a literal `define_method(:x)`, and the new name of
|
|
4876
|
+
# an `alias_method :x, :y` call. A non-literal name replaces nothing Rigor can name.
|
|
4877
|
+
def call_superseded_names(call_node)
|
|
4878
|
+
if (macro = attr_macro(call_node))
|
|
4879
|
+
names = []
|
|
4880
|
+
each_attr_macro_name(call_node) do |base|
|
|
4881
|
+
names << base unless macro == :attr_writer
|
|
4882
|
+
names << :"#{base}=" unless macro == :attr_reader
|
|
4883
|
+
end
|
|
4884
|
+
names
|
|
4885
|
+
elsif call_node.name == :define_method
|
|
4886
|
+
define_method_superseded_name(call_node)
|
|
4887
|
+
else
|
|
4888
|
+
Source::AliasNames.alias_method_call_names(call_node)&.first(1) || []
|
|
4889
|
+
end
|
|
4890
|
+
end
|
|
4891
|
+
|
|
4892
|
+
def define_method_superseded_name(call_node)
|
|
4893
|
+
return [] unless call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
|
|
4894
|
+
|
|
4895
|
+
name = literal_method_name(call_node.arguments&.arguments&.first)
|
|
4896
|
+
name.nil? ? [] : [name.to_sym]
|
|
4897
|
+
end
|
|
4898
|
+
|
|
4899
|
+
# Issue #1989 — a definer that is not a `def` replaces the method an earlier `def` of the same name in this walk
|
|
4900
|
+
# installed: `def x = 1; attr_reader :x` answers the attr reader, so the `def` body must not type the call. The
|
|
4901
|
+
# slot keeps the `def` and is contested, certain definer or possible, so the readers that re-type a body
|
|
4902
|
+
# decline. The slot is not emptied: the file still writes it, so a sibling file's `def` of the name (which the
|
|
4903
|
+
# cross-file seed and the project fold would otherwise put there) cannot fill it. A `def` written after the
|
|
4904
|
+
# definer writes the slot again and clears the contest.
|
|
4905
|
+
def supersede_def_nodes(names, qualified_prefix, accumulator, tables)
|
|
4906
|
+
return if names.empty?
|
|
4907
|
+
|
|
4908
|
+
class_name = qualified_prefix.join("::")
|
|
4909
|
+
slot = accumulator[class_name]
|
|
4910
|
+
return if slot.nil?
|
|
4911
|
+
|
|
4912
|
+
names.each do |name|
|
|
4913
|
+
follow_slot_writer(tables, :contested_def_nodes, [class_name, name], false) if slot.key?(name)
|
|
4914
|
+
end
|
|
4915
|
+
end
|
|
4916
|
+
|
|
4917
|
+
# Issue #2006 — a `define_method(:x)` or `attr_*` call is a definer the alias pass reads in source order
|
|
4918
|
+
# ({#apply_alias_def_nodes}). An `alias_method` call, whose new name {#call_superseded_names} also lists, is
|
|
4919
|
+
# itself an alias: the pass records its copy, so the history must not hold it as a definer with no `def`.
|
|
4920
|
+
def note_call_definers(call_node, names, qualified_prefix, tables)
|
|
4921
|
+
return if names.empty? || qualified_prefix.empty? || call_node.name == :alias_method
|
|
4922
|
+
|
|
4923
|
+
class_name = qualified_prefix.join("::")
|
|
4924
|
+
names.each { |name| tables.wrote_definer(class_name, name, call_node, nil) }
|
|
4925
|
+
end
|
|
4926
|
+
|
|
4730
4927
|
# #319 — a `def` inside an anonymous `Class.new` / `Module.new` body ALSO stays in the `<toplevel>` table.
|
|
4731
4928
|
# Before the anonymous class had a name the body was walked with an empty prefix, so every such `def`
|
|
4732
4929
|
# landed there; that is the same leniency this key already grants a `def` nested in any other DSL block
|
|
@@ -4772,6 +4969,18 @@ module Rigor
|
|
|
4772
4969
|
# The singleton walk's accumulator: the `{class => {name => DefNode}}` table, plus the contested slots, which
|
|
4773
4970
|
# follow each slot's writer ({#follow_slot_writer}'s rule).
|
|
4774
4971
|
class SingletonDefTable < Hash
|
|
4972
|
+
# Issue #1936 — contests the slots of `names` (every slot of `class_name` for `:all`) that an earlier write
|
|
4973
|
+
# filled: a later non-`def` definer replaced the `def` they hold. The slot keeps the `def`, as
|
|
4974
|
+
# {ScopeIndexer#supersede_def_nodes} keeps the instance one, and a later write clears the contest.
|
|
4975
|
+
def supersede(class_name, names)
|
|
4976
|
+
slot = self[class_name]
|
|
4977
|
+
return if slot.nil?
|
|
4978
|
+
|
|
4979
|
+
(names == :all ? slot.keys : names).each do |name|
|
|
4980
|
+
(@contested ||= Set.new) << [class_name, name] if slot.key?(name)
|
|
4981
|
+
end
|
|
4982
|
+
end
|
|
4983
|
+
|
|
4775
4984
|
def track(certainty)
|
|
4776
4985
|
@certainty = certainty
|
|
4777
4986
|
@contested = nil
|
|
@@ -4779,9 +4988,11 @@ module Rigor
|
|
|
4779
4988
|
end
|
|
4780
4989
|
|
|
4781
4990
|
# Writes the slot; `node` and `other` are the contributions the write rests on, possible when either is.
|
|
4782
|
-
|
|
4991
|
+
# `possible` says the write rests on a fact the contributions do not carry (a module_function copy after a
|
|
4992
|
+
# reset that may not run, #1968).
|
|
4993
|
+
def write(class_name, name, def_node, node, other = nil, possible: false)
|
|
4783
4994
|
(self[class_name] ||= {})[name] = def_node
|
|
4784
|
-
if Certainty.possible?(@certainty, node) || (other && Certainty.possible?(@certainty, other))
|
|
4995
|
+
if possible || Certainty.possible?(@certainty, node) || (other && Certainty.possible?(@certainty, other))
|
|
4785
4996
|
(@contested ||= Set.new) << [class_name, name]
|
|
4786
4997
|
else
|
|
4787
4998
|
@contested&.delete([class_name, name])
|
|
@@ -4824,6 +5035,12 @@ module Rigor
|
|
|
4824
5035
|
# `Const`'s singleton class (a `remove_method`, a `def`) without declaring `Const`, so it stays out of
|
|
4825
5036
|
# `class_sources` (whose readers take a name there as one the project declares) and joins only the per-file
|
|
4826
5037
|
# class-declaration sets the incremental `class:` edges diff (`Runner#class_declarations`).
|
|
5038
|
+
#
|
|
5039
|
+
# Issue #1941 — so does a `def Const.x` (`def D.own`, or `def E.x` inside `module M`, resolved as a `class << E`
|
|
5040
|
+
# receiver is): it adds to `Const`'s singleton side from a file the reader's class edge need not name. In the set,
|
|
5041
|
+
# the declaration diff fires `class:` when the file gains or loses one, and
|
|
5042
|
+
# `IncrementalSession#singleton_rewrite_affected` fires it when one is renamed. `def (M::E).x` records no
|
|
5043
|
+
# singleton `def` at all, so it joins nothing.
|
|
4827
5044
|
def record_singleton_class_sources(sources, path, root)
|
|
4828
5045
|
collect_singleton_class_targets(root, []) { |name| (sources[name] ||= Set.new) << path }
|
|
4829
5046
|
end
|
|
@@ -4839,6 +5056,8 @@ module Rigor
|
|
|
4839
5056
|
yield eval_constant_receiver_prefix(node, node.expression, rendered, lexical_prefix).join("::") if rendered
|
|
4840
5057
|
when Prism::CallNode
|
|
4841
5058
|
yield_singleton_side_constant(node, node.receiver, lexical_prefix, &) if singleton_side_call?(node)
|
|
5059
|
+
when Prism::DefNode
|
|
5060
|
+
yield_singleton_side_constant(node, node.receiver, lexical_prefix, &)
|
|
4842
5061
|
end
|
|
4843
5062
|
node.rigor_each_child { |child| collect_singleton_class_targets(child, lexical_prefix, &) }
|
|
4844
5063
|
end
|
|
@@ -4846,8 +5065,7 @@ module Rigor
|
|
|
4846
5065
|
# A call on a constant that may rewrite its singleton side (`Const.singleton_class`,
|
|
4847
5066
|
# `Const.define_singleton_method`, `Const.extend(M)`, `Const.instance_eval { … }`) records facts about `Const`'s
|
|
4848
5067
|
# singleton methods the refinement walk reads from a file that need not declare `Const`, so the file joins the
|
|
4849
|
-
# constant's set too. Written inside `Const`'s own body, the name is in the file's set already
|
|
4850
|
-
# files its own `method:` key.
|
|
5068
|
+
# constant's set too. Written inside `Const`'s own body, the name is in the file's set already.
|
|
4851
5069
|
def singleton_side_call?(node)
|
|
4852
5070
|
node.name == :singleton_class || surface_rewriting_call?(node)
|
|
4853
5071
|
end
|
|
@@ -4871,7 +5089,7 @@ module Rigor
|
|
|
4871
5089
|
# nested `def` or block inside its body is still a range the containment half needs.
|
|
4872
5090
|
# `def_owner_prefix` is the eval-body override described on {#walk_methods_and_def_nodes}:
|
|
4873
5091
|
# an explicit owner for def rows while declarations stay lexical (`[]` = ownerless).
|
|
4874
|
-
def walk_deferred_ranges(node, qualified_prefix, in_singleton_class, inside_deferred, # rubocop:disable Metrics/ParameterLists
|
|
5092
|
+
def walk_deferred_ranges(node, qualified_prefix, in_singleton_class, inside_deferred, # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
4875
5093
|
mf_offsets, ranges, def_owner_prefix = nil, singleton_cref: false,
|
|
4876
5094
|
defs_singleton: false)
|
|
4877
5095
|
return unless node.is_a?(Prism::Node)
|
|
@@ -4907,11 +5125,34 @@ module Rigor
|
|
|
4907
5125
|
singleton_cref: singleton_cref)
|
|
4908
5126
|
end
|
|
4909
5127
|
|
|
5128
|
+
record_definer_name_row(node, ranges) if node.is_a?(Prism::CallNode)
|
|
4910
5129
|
walk_deferred_children(node, qualified_prefix, in_singleton_class, inside_deferred,
|
|
4911
5130
|
mf_offsets, ranges, def_owner_prefix, singleton_cref: singleton_cref,
|
|
4912
5131
|
defs_singleton: defs_singleton)
|
|
4913
5132
|
end
|
|
4914
5133
|
|
|
5134
|
+
DEFINER_NAME_CALLS = Set[:define_method, :define_singleton_method, :alias_method].freeze
|
|
5135
|
+
DEFINER_SEND_CALLS = Set[:send, :__send__, :public_send].freeze
|
|
5136
|
+
private_constant :DEFINER_NAME_CALLS, :DEFINER_SEND_CALLS
|
|
5137
|
+
|
|
5138
|
+
# Issue #2184 — a `define_method` / `define_singleton_method` / `alias_method` call with a literal (new) name,
|
|
5139
|
+
# on any receiver and through `send` too, records an ownerless, kindless row over the name literal: no owner or
|
|
5140
|
+
# side is claimed, so no ordering scan matches it and the literal contains no call. It only tells a by-name
|
|
5141
|
+
# reader that the project gives the name a body no def table holds. A computed name records nothing.
|
|
5142
|
+
def record_definer_name_row(node, ranges)
|
|
5143
|
+
arguments = node.arguments&.arguments || []
|
|
5144
|
+
if DEFINER_SEND_CALLS.include?(node.name)
|
|
5145
|
+
return unless DEFINER_NAME_CALLS.include?(literal_method_name(arguments.first))
|
|
5146
|
+
|
|
5147
|
+
arguments = arguments.drop(1)
|
|
5148
|
+
end
|
|
5149
|
+
return unless DEFINER_NAME_CALLS.include?(node.name) || DEFINER_SEND_CALLS.include?(node.name)
|
|
5150
|
+
|
|
5151
|
+
argument = arguments.first
|
|
5152
|
+
name = literal_method_name(argument)
|
|
5153
|
+
ranges << [argument.location.start_offset, argument.location.end_offset, name, nil, nil] if name
|
|
5154
|
+
end
|
|
5155
|
+
|
|
4915
5156
|
# The `def` arm of {#walk_deferred_ranges}: records the range row — singleton-side when the
|
|
4916
5157
|
# def sits in a `class <<` body or an `instance_eval` block (`defs_singleton`) — then walks
|
|
4917
5158
|
# the body as a deferred range.
|
|
@@ -5062,7 +5303,7 @@ module Rigor
|
|
|
5062
5303
|
# own `def` leaves additionally split on {INSTANCE_EVAL_CALLS}, whose `def`s bind on the
|
|
5063
5304
|
# receiver's singleton rather than the receiver.
|
|
5064
5305
|
def receiver_eval_call?(node)
|
|
5065
|
-
node.block.is_a?(Prism::BlockNode) && RECEIVER_EVAL_CALLS.include?(node.name)
|
|
5306
|
+
node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode) && RECEIVER_EVAL_CALLS.include?(node.name)
|
|
5066
5307
|
end
|
|
5067
5308
|
|
|
5068
5309
|
# A `class_eval` / `module_eval` / `class_exec` / `module_exec` block is NOT deferred: it runs
|
|
@@ -5422,7 +5663,9 @@ module Rigor
|
|
|
5422
5663
|
mf_offsets, ranges)
|
|
5423
5664
|
start = def_node.location.start_offset
|
|
5424
5665
|
if inside_deferred
|
|
5425
|
-
|
|
5666
|
+
# Issue #2184 — the def installs at invocation under a `self` the walk does not know, so the row names the
|
|
5667
|
+
# method but claims no owner or side.
|
|
5668
|
+
ranges << [start, def_node.location.end_offset, def_node.name, nil, nil]
|
|
5426
5669
|
return
|
|
5427
5670
|
end
|
|
5428
5671
|
|
|
@@ -5445,7 +5688,10 @@ module Rigor
|
|
|
5445
5688
|
def_owner_prefix = nil, singleton_cref: false,
|
|
5446
5689
|
defs_singleton: false)
|
|
5447
5690
|
return unless node.is_a?(Prism::Node)
|
|
5448
|
-
|
|
5691
|
+
|
|
5692
|
+
supersede_singleton_def_nodes(node, qualified_prefix, in_singleton_class, defs_singleton, accumulator,
|
|
5693
|
+
def_owner_prefix)
|
|
5694
|
+
if receiver_eval_call?(node)
|
|
5449
5695
|
return walk_eval_singleton_defs(node, qualified_prefix, in_singleton_class, accumulator,
|
|
5450
5696
|
def_owner_prefix, singleton_cref: singleton_cref,
|
|
5451
5697
|
defs_singleton: defs_singleton)
|
|
@@ -5736,7 +5982,8 @@ module Rigor
|
|
|
5736
5982
|
defs_singleton: false)
|
|
5737
5983
|
owner_prefix = def_owner_prefix || qualified_prefix
|
|
5738
5984
|
statements = statements_of(body)
|
|
5739
|
-
ModuleFunctionState.
|
|
5985
|
+
conditional = ModuleFunctionState.conditional_clause_statements(body)
|
|
5986
|
+
ModuleFunctionState.each_singleton_sibling(statements, conditional) do |stmt, module_function_on, named_call|
|
|
5740
5987
|
if named_call
|
|
5741
5988
|
# `:unnameable` — the named defs the call copies live on the metaclass,
|
|
5742
5989
|
# which this table cannot name either.
|
|
@@ -5786,19 +6033,96 @@ module Rigor
|
|
|
5786
6033
|
statements
|
|
5787
6034
|
end
|
|
5788
6035
|
|
|
6036
|
+
# `module_function_on` is {ModuleFunctionState.each_singleton_sibling}'s: `:uncertain` writes the module
|
|
6037
|
+
# function's copy as possible, so the slot holds the later def, contested, rather than the def the
|
|
6038
|
+
# slot held before (#1968).
|
|
5789
6039
|
def record_singleton_def_node(def_node, qualified_prefix, in_singleton_class, module_function_on, accumulator)
|
|
5790
|
-
|
|
5791
|
-
return unless
|
|
6040
|
+
declared = def_singleton?(def_node, qualified_prefix, in_singleton_class)
|
|
6041
|
+
return unless declared || module_function_on
|
|
5792
6042
|
return if qualified_prefix.empty?
|
|
5793
6043
|
|
|
5794
6044
|
class_name = qualified_prefix.join("::")
|
|
5795
|
-
write_singleton_slot(accumulator, class_name, def_node.name, def_node, def_node
|
|
6045
|
+
write_singleton_slot(accumulator, class_name, def_node.name, def_node, def_node,
|
|
6046
|
+
possible: !declared && module_function_on == :uncertain)
|
|
6047
|
+
end
|
|
6048
|
+
|
|
6049
|
+
# Issue #1936 — the singleton twin of {#supersede_def_nodes}: a definer that is not a `def` replaces the
|
|
6050
|
+
# singleton method an earlier `def self.x` of the walk installed, so the slot is contested and the typing read
|
|
6051
|
+
# declines instead of re-typing the replaced body. The definers, each written on the class's own singleton
|
|
6052
|
+
# class: `singleton_class.alias_method` / `define_method` / `remove_method` / `undef_method` / `attr_*` and its
|
|
6053
|
+
# `class_eval` family and `send` (bare, `self.` or a constant's `singleton_class`), `define_singleton_method`,
|
|
6054
|
+
# and in a `class << self` body the receiverless calls of the same names with `alias` and `undef`. Certain or
|
|
6055
|
+
# possible alike; a call whose rewritten names are not all literal contests every slot the owner holds so far.
|
|
6056
|
+
# Under an `instance_eval` split (`defs_singleton`) only `alias` and `undef` bind on the singleton; its calls
|
|
6057
|
+
# stay instance-side.
|
|
6058
|
+
def supersede_singleton_def_nodes(node, qualified_prefix, in_singleton_class, defs_singleton, accumulator,
|
|
6059
|
+
def_owner_prefix)
|
|
6060
|
+
return unless accumulator.is_a?(SingletonDefTable) && defs_singleton != :unnameable
|
|
6061
|
+
|
|
6062
|
+
owner_prefix = def_owner_prefix || qualified_prefix
|
|
6063
|
+
current = owner_prefix.empty? ? nil : owner_prefix.join("::")
|
|
6064
|
+
owner, names =
|
|
6065
|
+
case node
|
|
6066
|
+
when Prism::AliasMethodNode, Prism::UndefNode
|
|
6067
|
+
[current, singleton_keyword_names(node)] if in_singleton_class || defs_singleton == true
|
|
6068
|
+
when Prism::CallNode
|
|
6069
|
+
singleton_call_target(node, qualified_prefix, current, in_singleton_class)
|
|
6070
|
+
end
|
|
6071
|
+
accumulator.supersede(owner, names) if owner && names
|
|
6072
|
+
end
|
|
6073
|
+
|
|
6074
|
+
def singleton_keyword_names(node)
|
|
6075
|
+
return literal_names_or_all(node.names) if node.is_a?(Prism::UndefNode)
|
|
6076
|
+
|
|
6077
|
+
names = Source::AliasNames.keyword_names(node)
|
|
6078
|
+
names ? names.first(1) : :all
|
|
6079
|
+
end
|
|
6080
|
+
|
|
6081
|
+
# `[owner, names]` for a singleton definer call {#supersede_singleton_def_nodes} reads, or nil.
|
|
6082
|
+
def singleton_call_target(node, qualified_prefix, current, in_singleton_class)
|
|
6083
|
+
receiver = node.receiver
|
|
6084
|
+
if !in_singleton_class && singleton_class_of_receiver?(receiver)
|
|
6085
|
+
[singleton_class_owner(node, receiver.receiver, qualified_prefix, current), singleton_naming_call_names(node)]
|
|
6086
|
+
elsif receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
6087
|
+
[current, in_singleton_class ? singleton_naming_call_names(node) : define_singleton_method_names(node)]
|
|
6088
|
+
end
|
|
6089
|
+
end
|
|
6090
|
+
|
|
6091
|
+
# The singleton names a naming call on the singleton class replaces, `:all` where they are not all literal,
|
|
6092
|
+
# or nil for another call. `attr_writer` replaces only `x=`, `attr_reader` only `x`. A `class_eval`-family or
|
|
6093
|
+
# `send` call may replace any name, so it is `:all`.
|
|
6094
|
+
def singleton_naming_call_names(node)
|
|
6095
|
+
arguments = node.arguments&.arguments || []
|
|
6096
|
+
case node.name
|
|
6097
|
+
when *SURFACE_EVAL_CALLS, *SURFACE_SEND_CALLS then :all
|
|
6098
|
+
when :alias_method then literal_names_or_all(arguments.first(1))
|
|
6099
|
+
when :define_method then define_singleton_method_names(node, :define_method)
|
|
6100
|
+
when :remove_method, :undef_method then literal_names_or_all(arguments)
|
|
6101
|
+
when *ATTR_MACROS
|
|
6102
|
+
names = literal_names_or_all(arguments)
|
|
6103
|
+
return names if names == :all
|
|
6104
|
+
|
|
6105
|
+
names.flat_map do |base|
|
|
6106
|
+
[(base unless node.name == :attr_writer), (:"#{base}=" unless node.name == :attr_reader)].compact
|
|
6107
|
+
end
|
|
6108
|
+
end
|
|
6109
|
+
end
|
|
6110
|
+
|
|
6111
|
+
def define_singleton_method_names(node, call = :define_singleton_method)
|
|
6112
|
+
return nil unless node.name == call
|
|
6113
|
+
|
|
6114
|
+
literal_names_or_all(node.arguments&.arguments&.first(1) || [])
|
|
6115
|
+
end
|
|
6116
|
+
|
|
6117
|
+
def literal_names_or_all(arguments)
|
|
6118
|
+
names = arguments.map { |argument| literal_method_name(argument) }
|
|
6119
|
+
names.empty? || names.any?(&:nil?) ? :all : names
|
|
5796
6120
|
end
|
|
5797
6121
|
|
|
5798
6122
|
# One singleton slot write, through {SingletonDefTable#write} when the walk tracks certainty.
|
|
5799
|
-
def write_singleton_slot(accumulator, class_name, name, def_node, node, other = nil)
|
|
6123
|
+
def write_singleton_slot(accumulator, class_name, name, def_node, node, other = nil, possible: false)
|
|
5800
6124
|
if accumulator.is_a?(SingletonDefTable)
|
|
5801
|
-
accumulator.write(class_name, name, def_node, node, other)
|
|
6125
|
+
accumulator.write(class_name, name, def_node, node, other, possible: possible)
|
|
5802
6126
|
else
|
|
5803
6127
|
(accumulator[class_name] ||= {})[name] = def_node
|
|
5804
6128
|
end
|
|
@@ -6695,32 +7019,34 @@ module Rigor
|
|
|
6695
7019
|
# differently for the two spellings of one edge.
|
|
6696
7020
|
def record_mixin_call(node, qualified_prefix, current_class, accumulator)
|
|
6697
7021
|
taint_opaque_eval(node, current_class, accumulator)
|
|
6698
|
-
|
|
6699
|
-
return if
|
|
7022
|
+
view = mixin_call_view(node)
|
|
7023
|
+
return if view.nil? || view.first.nil?
|
|
6700
7024
|
|
|
7025
|
+
kind = view.first
|
|
6701
7026
|
current_class ||= toplevel_include_owner(node, qualified_prefix, current_class, accumulator)
|
|
6702
7027
|
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
6703
7028
|
.select { |_owner, side| side == :include }
|
|
6704
7029
|
return if effects.empty?
|
|
6705
|
-
return if
|
|
7030
|
+
return if instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
|
|
6706
7031
|
|
|
6707
7032
|
# A mixin call the tables cannot hold still reshapes the ancestry: taint the whole owner side.
|
|
6708
7033
|
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
6709
7034
|
end
|
|
6710
7035
|
|
|
6711
7036
|
# Records the instance-side edges of a call {#mixin_call_recorded?} accepts. False when it recorded
|
|
6712
|
-
# nothing — the caller then taints the owner instead.
|
|
6713
|
-
|
|
6714
|
-
|
|
7037
|
+
# nothing — the caller then taints the owner instead. `view` is {#mixin_call_view}'s, so
|
|
7038
|
+
# `Integer.send(:include, Ext)` records `Ext` as `Integer.include(Ext)` does (issue #1722).
|
|
7039
|
+
def instance_mixin_recorded?(node, view, qualified_prefix, current_class, accumulator)
|
|
7040
|
+
kind, arguments, via_send = view
|
|
7041
|
+
return false unless mixin_call_recorded?(node, kind, via_send, current_class)
|
|
6715
7042
|
|
|
6716
|
-
arguments = node.arguments&.arguments || []
|
|
6717
7043
|
targets = arguments.filter_map { |arg| Source::ConstantPath.qualified_name(arg) }
|
|
6718
7044
|
return false if targets.empty?
|
|
6719
7045
|
|
|
6720
7046
|
owner = node.receiver.nil? ? current_class : prepend_call_receiver(node, qualified_prefix)
|
|
6721
7047
|
return false if owner.nil?
|
|
6722
7048
|
|
|
6723
|
-
write_mixin_targets(accumulator, owner, targets, prepend:
|
|
7049
|
+
write_mixin_targets(accumulator, owner, targets, prepend: kind == :prepend)
|
|
6724
7050
|
accumulator.note(node, owner, :include, targets, complete: targets.size == arguments.size)
|
|
6725
7051
|
true
|
|
6726
7052
|
end
|
|
@@ -6868,15 +7194,17 @@ module Rigor
|
|
|
6868
7194
|
end
|
|
6869
7195
|
|
|
6870
7196
|
# Whether a mixin call contributes to the tables at all: a receiverless `include` / `prepend` needs an
|
|
6871
|
-
# enclosing declaration to file the edge under, and the RECEIVER form
|
|
6872
|
-
#
|
|
6873
|
-
#
|
|
6874
|
-
#
|
|
6875
|
-
|
|
6876
|
-
|
|
6877
|
-
|
|
7197
|
+
# enclosing declaration to file the edge under, and the RECEIVER form (`Base.prepend(Loud)`, issue #1123;
|
|
7198
|
+
# `Integer.include(Ext)` and `Integer.send(:include, Ext)`, issue #1722) files it under the class the
|
|
7199
|
+
# receiver names. A receiver-form edge is unpositioned wherever it is written ({MixinAccumulator#note}), so
|
|
7200
|
+
# a reader that depends on ORDER declines on it, while the set-shaped readers (undefined-method
|
|
7201
|
+
# suppression, arity, visibility) see the module as they see an in-body `include`. A receiverless `send`
|
|
7202
|
+
# stays a taint: the walk does not resolve its `self`.
|
|
7203
|
+
def mixin_call_recorded?(node, kind, via_send, current_class)
|
|
7204
|
+
return false unless MIXIN_CALL_NAMES.include?(kind)
|
|
7205
|
+
return !via_send && !current_class.nil? if node.receiver.nil?
|
|
6878
7206
|
|
|
6879
|
-
|
|
7207
|
+
true
|
|
6880
7208
|
end
|
|
6881
7209
|
|
|
6882
7210
|
# Issue #1697 — a receiverless `include M` written as a statement of the file's own top level is
|
|
@@ -7315,7 +7643,8 @@ module Rigor
|
|
|
7315
7643
|
return record_module_function(node, current_class, accumulator, in_singleton) if node.name == :module_function
|
|
7316
7644
|
|
|
7317
7645
|
taint_opaque_eval(node, current_class, accumulator)
|
|
7318
|
-
return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator
|
|
7646
|
+
return if singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator,
|
|
7647
|
+
in_singleton: in_singleton)
|
|
7319
7648
|
|
|
7320
7649
|
kind, _arguments, via_send = mixin_call_view(node)
|
|
7321
7650
|
return if kind.nil?
|
|
@@ -7323,7 +7652,8 @@ module Rigor
|
|
|
7323
7652
|
effects = mixin_effects(node, kind, qualified_prefix, current_class, accumulator, in_singleton: in_singleton)
|
|
7324
7653
|
.select { |_owner, side| side == :extend || in_singleton }
|
|
7325
7654
|
return if effects.empty?
|
|
7326
|
-
return if !via_send && singleton_mixin_recorded?(node, kind, current_class, accumulator,
|
|
7655
|
+
return if !via_send && singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator,
|
|
7656
|
+
in_singleton)
|
|
7327
7657
|
|
|
7328
7658
|
effects.each { |owner, side| accumulator.taint(owner, side) }
|
|
7329
7659
|
end
|
|
@@ -7338,7 +7668,7 @@ module Rigor
|
|
|
7338
7668
|
# but only where it is a direct statement (a top-level statement, or one of a direct declaration body or of a
|
|
7339
7669
|
# block that runs there once): there the methods walk records those marks under the same owner. In a method
|
|
7340
7670
|
# body, a hook or a conditional it may run on a receiver or at a time the marks cannot place, so it still taints.
|
|
7341
|
-
def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator)
|
|
7671
|
+
def singleton_class_rewrite_tainted?(node, qualified_prefix, current_class, accumulator, in_singleton: false)
|
|
7342
7672
|
receiver = node.receiver
|
|
7343
7673
|
return false unless singleton_class_of_receiver?(receiver) && surface_rewriting_call?(node)
|
|
7344
7674
|
return false if singleton_class_named_call?(node) && accumulator.direct?(node)
|
|
@@ -7346,10 +7676,33 @@ module Rigor
|
|
|
7346
7676
|
owner = singleton_class_owner(node, receiver.receiver, qualified_prefix, current_class)
|
|
7347
7677
|
return false if owner.nil?
|
|
7348
7678
|
|
|
7679
|
+
# Inside `class << self` a bare `singleton_class` is the singleton's own singleton, one level up from the
|
|
7680
|
+
# owner's extend side, so that call stays a taint.
|
|
7681
|
+
up_level = in_singleton && lexical_self_receiver?(receiver.receiver)
|
|
7682
|
+
return true if !up_level && singleton_class_include_recorded?(node, owner, accumulator)
|
|
7683
|
+
|
|
7349
7684
|
accumulator.taint(owner, :extend)
|
|
7350
7685
|
true
|
|
7351
7686
|
end
|
|
7352
7687
|
|
|
7688
|
+
# Issue #2094 — `Float.singleton_class.include(Ext)` (or `prepend`, which the extend table cannot order, so the
|
|
7689
|
+
# edge is unpositioned as every receiver-form edge is) puts `Ext` on the singleton side as `Float.extend(Ext)`
|
|
7690
|
+
# does, the fold #915 gives `class << self; include M`. A `send` spelling or an argument that is not a constant
|
|
7691
|
+
# stays a taint.
|
|
7692
|
+
def singleton_class_include_recorded?(node, owner, accumulator)
|
|
7693
|
+
return false unless %i[include prepend].include?(node.name)
|
|
7694
|
+
|
|
7695
|
+
arguments = node.arguments&.arguments || []
|
|
7696
|
+
return false if arguments.empty? || arguments.any? { |arg| Source::ConstantPath.qualified_name(arg).nil? }
|
|
7697
|
+
|
|
7698
|
+
!record_extend_targets(node, owner, accumulator).empty?
|
|
7699
|
+
end
|
|
7700
|
+
|
|
7701
|
+
# A bare or `self.` receiver: the lexical `self`.
|
|
7702
|
+
def lexical_self_receiver?(receiver)
|
|
7703
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
7704
|
+
end
|
|
7705
|
+
|
|
7353
7706
|
# A bare, `self.` or constant receiver's `singleton_class` call.
|
|
7354
7707
|
def singleton_class_of_receiver?(receiver)
|
|
7355
7708
|
receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class && receiver.arguments.nil?
|
|
@@ -7408,13 +7761,29 @@ module Rigor
|
|
|
7408
7761
|
|
|
7409
7762
|
# Records the singleton-side edges of a receiverless `extend` (or, inside `class << self`, `include` /
|
|
7410
7763
|
# `prepend`). False when it recorded nothing.
|
|
7411
|
-
def singleton_mixin_recorded?(node, kind, current_class, accumulator, in_singleton)
|
|
7412
|
-
return
|
|
7764
|
+
def singleton_mixin_recorded?(node, kind, qualified_prefix, current_class, accumulator, in_singleton)
|
|
7765
|
+
return receiver_extend_recorded?(node, kind, qualified_prefix, accumulator) if node.receiver
|
|
7766
|
+
return false unless current_class
|
|
7413
7767
|
return false unless in_singleton ? MIXIN_CALL_NAMES.include?(kind) : kind == :extend
|
|
7414
7768
|
|
|
7415
7769
|
!record_extend_targets(node, current_class, accumulator).empty?
|
|
7416
7770
|
end
|
|
7417
7771
|
|
|
7772
|
+
# Issue #2094 — the receiver form, `Float.extend(Ext)`: the singleton twin of #1722's `Integer.include(Ext)`,
|
|
7773
|
+
# filed under the class the constant receiver names ({#prepend_call_receiver}) and, like that form,
|
|
7774
|
+
# unpositioned wherever it is written ({MixinAccumulator#note}). A `self` argument names the receiver, not the
|
|
7775
|
+
# enclosing class, so a call with one stays a taint. A constant receiver names the same class inside
|
|
7776
|
+
# `class << self`, so the walk's singleton flag does not matter here.
|
|
7777
|
+
def receiver_extend_recorded?(node, kind, qualified_prefix, accumulator)
|
|
7778
|
+
return false if kind != :extend
|
|
7779
|
+
return false if (node.arguments&.arguments || []).any?(Prism::SelfNode)
|
|
7780
|
+
|
|
7781
|
+
owner = prepend_call_receiver(node, qualified_prefix)
|
|
7782
|
+
return false if owner.nil?
|
|
7783
|
+
|
|
7784
|
+
!record_extend_targets(node, owner, accumulator).empty?
|
|
7785
|
+
end
|
|
7786
|
+
|
|
7418
7787
|
# The table stores search order, not call order: a later `extend` statement prepends its module
|
|
7419
7788
|
# (`extend A; extend B` → singleton ancestors `[B, A]`) while the arguments of a single
|
|
7420
7789
|
# `extend A, B` keep call order (`[A, B]`). Prepending each call's argument list preserves both,
|
|
@@ -7589,6 +7958,8 @@ module Rigor
|
|
|
7589
7958
|
# - **Wrap-around form** (#1911, #1819, #1633): `private def foo` and `private attr_reader :foo` record the
|
|
7590
7959
|
# members with the call's visibility and leave the default alone.
|
|
7591
7960
|
#
|
|
7961
|
+
# A `module_function` call takes each form as `private` does (#1569): Ruby makes the instance copy private.
|
|
7962
|
+
#
|
|
7592
7963
|
# Top-level (no surrounding class) defs do not contribute — Ruby's top-level visibility nuances (private at
|
|
7593
7964
|
# top-level marks the method on `Object`) are out of scope for v0.1.2.
|
|
7594
7965
|
def build_discovered_method_visibilities(root)
|
|
@@ -7605,8 +7976,8 @@ module Rigor
|
|
|
7605
7976
|
end
|
|
7606
7977
|
|
|
7607
7978
|
# The bare calls that set the running default visibility of the body they run in: the modifiers and
|
|
7608
|
-
# `module_function`, which {#walk_method_visibilities}
|
|
7609
|
-
#
|
|
7979
|
+
# `module_function`, which {#walk_method_visibilities} applies as `private` on the instance side
|
|
7980
|
+
# ({ModuleFunctionState.instance_visibility}, #1569).
|
|
7610
7981
|
VISIBILITY_TOGGLES = (VISIBILITY_MODIFIERS + %i[module_function]).freeze
|
|
7611
7982
|
|
|
7612
7983
|
# The `{class => {name => visibility}}` accumulator of {#walk_method_visibilities}, which also carries the
|
|
@@ -7644,15 +8015,38 @@ module Rigor
|
|
|
7644
8015
|
end
|
|
7645
8016
|
end
|
|
7646
8017
|
|
|
8018
|
+
# Issue #2013 — contests the instance slot of `name`, recorded or not, and marks the contest as a
|
|
8019
|
+
# replacement's ({ScopeIndexer.replaced_visibility_mark}): a `define_method(:name)` or an alias gave the name
|
|
8020
|
+
# a visibility the walk does not read, and another file may record the slot. A later certain write in this
|
|
8021
|
+
# walk clears the contest ({#wrote}) and leaves the mark, which then only keeps a contest another file puts on
|
|
8022
|
+
# the slot: the fold declines more, never reports more, and file order is not load order there either.
|
|
8023
|
+
def contest_replaced(class_name, name)
|
|
8024
|
+
contest_slot(class_name, name)
|
|
8025
|
+
contest_slot(class_name, ScopeIndexer.replaced_visibility_mark(name))
|
|
8026
|
+
end
|
|
8027
|
+
|
|
7647
8028
|
# Contests the `[class_name, name]` slot, recorded or not: a visibility call changed it in a way the walk
|
|
7648
8029
|
# cannot read (`private def self.x` reaching an instance `x`).
|
|
7649
8030
|
def contest_slot(class_name, name)
|
|
7650
8031
|
(@contested ||= Set.new) << [class_name, name].freeze
|
|
7651
8032
|
end
|
|
7652
8033
|
|
|
7653
|
-
# Contests every slot of `class_name` recorded so far (`private(*names)` may name any of them).
|
|
7654
|
-
|
|
7655
|
-
|
|
8034
|
+
# Contests every slot of `class_name` on `side` recorded so far (`private(*names)` may name any of them).
|
|
8035
|
+
# Issue #1984 — on the singleton side it also contests the class's wildcard slot
|
|
8036
|
+
# ({ScopeIndexer::SINGLETON_WILDCARD_SLOT}): the names may be ones another file records.
|
|
8037
|
+
def contest_class(class_name, side = :instance)
|
|
8038
|
+
self[class_name]&.each_key do |slot|
|
|
8039
|
+
contest_slot(class_name, slot) if slot.is_a?(Array) == (side == :singleton)
|
|
8040
|
+
end
|
|
8041
|
+
contest_slot(class_name, SINGLETON_WILDCARD_SLOT) if side == :singleton
|
|
8042
|
+
end
|
|
8043
|
+
|
|
8044
|
+
# Issue #1984 — records `visibility` in the slot of `name` on `side` ({ScopeIndexer.visibility_slot}) and
|
|
8045
|
+
# settles its contest as {#wrote} does.
|
|
8046
|
+
def write(class_name, name, side, visibility, node, toggled:)
|
|
8047
|
+
slot = ScopeIndexer.visibility_slot(name, side)
|
|
8048
|
+
(self[class_name] ||= {})[slot] = visibility
|
|
8049
|
+
wrote(class_name, slot, node, toggled: toggled)
|
|
7656
8050
|
end
|
|
7657
8051
|
|
|
7658
8052
|
# Runs the block with the running default uncertain, so a `def` it records is contested.
|
|
@@ -7769,10 +8163,12 @@ module Rigor
|
|
|
7769
8163
|
return current_visibility
|
|
7770
8164
|
end
|
|
7771
8165
|
when Prism::DefNode
|
|
7772
|
-
|
|
8166
|
+
contest_foreign_singleton_visibility(node, qualified_prefix, owner_prefix, in_singleton_class, accumulator)
|
|
8167
|
+
record_def_visibility(node, owner_prefix, visibility_side(in_singleton_class, defs_singleton),
|
|
7773
8168
|
current_visibility, accumulator)
|
|
7774
8169
|
return current_visibility
|
|
7775
8170
|
when Prism::CallNode
|
|
8171
|
+
record_constant_class_method_calls(node, qualified_prefix, accumulator)
|
|
7776
8172
|
if receiver_eval_call?(node)
|
|
7777
8173
|
walk_eval_visibilities(node, qualified_prefix, in_singleton_class, current_visibility,
|
|
7778
8174
|
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
@@ -7787,23 +8183,50 @@ module Rigor
|
|
|
7787
8183
|
end
|
|
7788
8184
|
if (parts = visibility_call_parts(node))
|
|
7789
8185
|
accumulator.toggles += 1 if visibility_toggle_arguments?(parts.last)
|
|
7790
|
-
#
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
#
|
|
7794
|
-
#
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
8186
|
+
# A `module_function` call is applied as a `private` one (#1569): it makes the instance copy private.
|
|
8187
|
+
visibility = parts.first
|
|
8188
|
+
visibility = ModuleFunctionState.instance_visibility unless VISIBILITY_MODIFIERS.include?(visibility)
|
|
8189
|
+
# Issue #1984 — inside `class << self` (or a `class_eval` on the singleton class) the call marks the
|
|
8190
|
+
# SINGLETON methods, recorded under their `[:singleton, name]` slots. `module_function` is no method of
|
|
8191
|
+
# a singleton class, so it is walked as any other call there. The `owner_prefix` check is a guard: an
|
|
8192
|
+
# ownerless body (the top level, an anonymous factory block) records nothing, and its calls walk as
|
|
8193
|
+
# any other call.
|
|
8194
|
+
# An `instance_eval` body's call (`:eval`) runs on the receiver as a module: the instance side.
|
|
8195
|
+
side = visibility_side(in_singleton_class, defs_singleton)
|
|
8196
|
+
side = :instance if side == :eval
|
|
8197
|
+
if side && !owner_prefix.empty? && (side == :instance || VISIBILITY_MODIFIERS.include?(parts.first))
|
|
8198
|
+
return apply_visibility_call(node, visibility, parts.last, owner_prefix, current_visibility, accumulator,
|
|
8199
|
+
side: side, defs_singleton: defs_singleton) do |argument|
|
|
8200
|
+
walk_method_visibilities(argument, qualified_prefix, in_singleton_class, current_visibility,
|
|
8201
|
+
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
8202
|
+
defs_singleton: defs_singleton)
|
|
8203
|
+
end
|
|
8204
|
+
end
|
|
8205
|
+
end
|
|
8206
|
+
if !in_singleton_class && !owner_prefix.empty?
|
|
8207
|
+
class_method_parts = class_method_visibility_parts(node)
|
|
8208
|
+
if class_method_parts
|
|
8209
|
+
apply_class_method_visibility(node, *class_method_parts, owner_prefix, accumulator) do |argument|
|
|
7798
8210
|
walk_method_visibilities(argument, qualified_prefix, in_singleton_class, current_visibility,
|
|
7799
8211
|
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
7800
8212
|
defs_singleton: defs_singleton)
|
|
7801
8213
|
end
|
|
8214
|
+
return current_visibility
|
|
7802
8215
|
end
|
|
8216
|
+
contest_singleton_rewrites(node, owner_prefix, accumulator)
|
|
7803
8217
|
end
|
|
7804
|
-
|
|
7805
|
-
|
|
8218
|
+
side = visibility_side(in_singleton_class, defs_singleton)
|
|
8219
|
+
if %i[instance singleton].include?(side)
|
|
8220
|
+
record_attr_visibility(node, owner_prefix, current_visibility, accumulator, toggled: true, side: side)
|
|
8221
|
+
contest_replaced_visibility(replaced_by_call(node), owner_prefix, accumulator, side: side)
|
|
7806
8222
|
end
|
|
8223
|
+
when Prism::AliasMethodNode
|
|
8224
|
+
side = visibility_side(in_singleton_class, defs_singleton)
|
|
8225
|
+
if %i[instance singleton].include?(side)
|
|
8226
|
+
contest_replaced_visibility(Array(Source::AliasNames.keyword_names(node)&.first), owner_prefix,
|
|
8227
|
+
accumulator, side: side)
|
|
8228
|
+
end
|
|
8229
|
+
return current_visibility
|
|
7807
8230
|
end
|
|
7808
8231
|
|
|
7809
8232
|
# Statement-position StatementsNode preserves left-to-right visibility flow; everything else recurses with the
|
|
@@ -7887,6 +8310,9 @@ module Rigor
|
|
|
7887
8310
|
singleton_cref: singleton_cref,
|
|
7888
8311
|
defs_singleton: defs_singleton)
|
|
7889
8312
|
end
|
|
8313
|
+
return if singleton_class_eval_walked?(node, qualified_prefix, in_singleton_class, accumulator,
|
|
8314
|
+
def_owner_prefix, singleton_cref, defs_singleton)
|
|
8315
|
+
|
|
7890
8316
|
node.arguments&.arguments&.each do |arg|
|
|
7891
8317
|
walk_method_visibilities(arg, qualified_prefix, in_singleton_class, current_visibility,
|
|
7892
8318
|
accumulator, def_owner_prefix, singleton_cref: singleton_cref,
|
|
@@ -7911,63 +8337,191 @@ module Rigor
|
|
|
7911
8337
|
end
|
|
7912
8338
|
end
|
|
7913
8339
|
|
|
7914
|
-
#
|
|
7915
|
-
def
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
8340
|
+
# Issue #1984 — `singleton_class.class_eval do … end` in a class body (or `module_eval` / `class_exec` /
|
|
8341
|
+
# `module_exec`) runs its block as a `class << self` body does: its `def`s and visibility calls land on the
|
|
8342
|
+
# class's singleton slots. True when it walked the block so.
|
|
8343
|
+
def singleton_class_eval_walked?(node, qualified_prefix, in_singleton_class, accumulator,
|
|
8344
|
+
def_owner_prefix, singleton_cref, defs_singleton)
|
|
8345
|
+
return false if in_singleton_class || defs_singleton || INSTANCE_EVAL_CALLS.include?(node.name)
|
|
8346
|
+
return false unless own_singleton_class?(node.receiver) && node.arguments.nil?
|
|
8347
|
+
|
|
8348
|
+
owner_prefix = def_owner_prefix || qualified_prefix
|
|
8349
|
+
return false if owner_prefix.empty?
|
|
8350
|
+
|
|
8351
|
+
if node.block.body
|
|
8352
|
+
accumulator.body do
|
|
8353
|
+
walk_method_visibilities(node.block.body, qualified_prefix, true, :public, accumulator, owner_prefix,
|
|
8354
|
+
singleton_cref: singleton_cref)
|
|
8355
|
+
end
|
|
8356
|
+
end
|
|
8357
|
+
true
|
|
8358
|
+
end
|
|
8359
|
+
|
|
8360
|
+
# Issue #1984 — the slot a contest stands on for every singleton name of a class: a call that may make any of
|
|
8361
|
+
# them public (`K.public_class_method(*names)`, `K.singleton_class.class_eval { … }`).
|
|
8362
|
+
SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
|
|
8363
|
+
|
|
8364
|
+
# Issue #2013 — the mark a contested instance slot `name` carries in `contested_discovered_method_visibilities`
|
|
8365
|
+
# when a `define_method(:name)` or an alias replaced the method: `[:replaced, name]`. No reader asks for it; it
|
|
8366
|
+
# keeps the slot's contest when another file's write of the slot folds later ({#fold_visibility_contests}),
|
|
8367
|
+
# since file order is not load order.
|
|
8368
|
+
def self.replaced_visibility_mark(name) = [:replaced, name].freeze
|
|
8369
|
+
|
|
8370
|
+
# Issue #1984 — a class method visibility call written on a constant (`K.public_class_method(:x)`,
|
|
8371
|
+
# `K.send(:public_class_method, :x)`), anywhere: each literal name is recorded public on every class the
|
|
8372
|
+
# constant may name, so a reopening's or another file's `private_class_method` disagrees with it and is
|
|
8373
|
+
# contested. A `K.private_class_method` is not read (the rule only declines more where it is missing). Any call
|
|
8374
|
+
# on `K.singleton_class` (`send(:public, :x)`, `class_eval { … }`), or a `public_class_method` whose names are
|
|
8375
|
+
# not literal, contests every singleton name of the class.
|
|
8376
|
+
def record_constant_class_method_calls(node, qualified_prefix, accumulator)
|
|
8377
|
+
receiver = node.receiver
|
|
8378
|
+
if singleton_class_of_receiver?(receiver) && constant_receiver?(receiver.receiver)
|
|
8379
|
+
each_constant_class(receiver.receiver, qualified_prefix) do |class_name|
|
|
8380
|
+
accumulator.contest_class(class_name, :singleton)
|
|
8381
|
+
end
|
|
8382
|
+
return
|
|
8383
|
+
end
|
|
8384
|
+
names = constant_public_class_method_names(node)
|
|
8385
|
+
return if names.nil?
|
|
8386
|
+
|
|
8387
|
+
each_constant_class(receiver, qualified_prefix) do |class_name|
|
|
8388
|
+
next accumulator.contest_class(class_name, :singleton) if names.any?(&:nil?)
|
|
8389
|
+
|
|
8390
|
+
names.each do |method_name|
|
|
8391
|
+
accumulator.write(class_name, method_name, :singleton, :public, node, toggled: false)
|
|
8392
|
+
end
|
|
8393
|
+
end
|
|
8394
|
+
end
|
|
8395
|
+
|
|
8396
|
+
# The names a `K.public_class_method(…)` (or `K.send(:public_class_method, …)`) call on a constant names, a nil
|
|
8397
|
+
# for each one that is not a literal; nil for any other call.
|
|
8398
|
+
def constant_public_class_method_names(node)
|
|
8399
|
+
return nil unless constant_receiver?(node.receiver)
|
|
8400
|
+
|
|
8401
|
+
name = node.name
|
|
8402
|
+
arguments = node.arguments&.arguments || EMPTY_VISIBILITY_ARGUMENTS
|
|
8403
|
+
if VISIBILITY_SEND_CALLS.include?(name) || name == :public_send
|
|
8404
|
+
name = Source::Literals.symbol_or_string(arguments.first)
|
|
8405
|
+
arguments = arguments.drop(1)
|
|
8406
|
+
end
|
|
8407
|
+
return nil unless name == :public_class_method
|
|
8408
|
+
|
|
8409
|
+
arguments.flat_map { |arg| arg.is_a?(Prism::ArrayNode) ? arg.elements : [arg] }
|
|
8410
|
+
.map { |arg| Source::Literals.symbol_or_string(arg) }
|
|
8411
|
+
end
|
|
8412
|
+
|
|
8413
|
+
def constant_receiver?(node)
|
|
8414
|
+
node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
8415
|
+
end
|
|
8416
|
+
|
|
8417
|
+
def each_constant_class(receiver, qualified_prefix, &)
|
|
8418
|
+
constant_receiver_candidates(receiver, qualified_prefix).each(&)
|
|
8419
|
+
end
|
|
8420
|
+
|
|
8421
|
+
# Issue #1984 — the side a body's members land on: `:singleton` in a `class << self` body or a `class_eval` on
|
|
8422
|
+
# the singleton class, `:eval` in an `instance_eval` body on a class (its `def`s are singleton methods, its
|
|
8423
|
+
# visibility calls instance-side), `:instance` in a class body, and nil where the singleton cannot be named.
|
|
8424
|
+
def visibility_side(in_singleton_class, defs_singleton)
|
|
8425
|
+
return defs_singleton == :unnameable ? nil : :singleton if in_singleton_class
|
|
8426
|
+
return :eval if defs_singleton
|
|
8427
|
+
|
|
8428
|
+
:instance
|
|
8429
|
+
end
|
|
8430
|
+
|
|
8431
|
+
# Issue #1984 — the key of `name`'s slot in a class's visibility table: the name on the instance side,
|
|
8432
|
+
# `[:singleton, name]` on the singleton side (the envelope table's spelling).
|
|
8433
|
+
def visibility_slot(name, side)
|
|
8434
|
+
side == :singleton ? [:singleton, name].freeze : name
|
|
8435
|
+
end
|
|
8436
|
+
|
|
8437
|
+
# Records the visibility a `def` gets ({#visibility_side} `side`):
|
|
8438
|
+
#
|
|
8439
|
+
# - a receiverless `def` takes `current_visibility` on its side; `toggled` when that is the running default
|
|
8440
|
+
# (a statement), false for one a visibility call wraps;
|
|
8441
|
+
# - issue #1984 — `def self.x` (or `def K.x` in `K`'s body) is a public singleton method whatever the running
|
|
8442
|
+
# default is (`private; def self.x` is public), and writes nothing in a singleton body (it defines a method
|
|
8443
|
+
# of the singleton's singleton);
|
|
8444
|
+
# - an `instance_eval` body's `def` (`:eval`) replaces a singleton method with a visibility the walk does not
|
|
8445
|
+
# read, so its singleton slot is contested.
|
|
8446
|
+
def record_def_visibility(def_node, qualified_prefix, side, current_visibility, accumulator, toggled: true)
|
|
8447
|
+
return if side.nil? || qualified_prefix.empty?
|
|
7919
8448
|
|
|
7920
8449
|
class_name = qualified_prefix.join("::")
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
8450
|
+
if side == :eval
|
|
8451
|
+
accumulator.contest_slot(class_name, visibility_slot(def_node.name, :singleton))
|
|
8452
|
+
elsif def_node.receiver.nil?
|
|
8453
|
+
accumulator.write(class_name, def_node.name, side, current_visibility, def_node, toggled: toggled)
|
|
8454
|
+
elsif side == :instance && self_singleton_def?(def_node, qualified_prefix)
|
|
8455
|
+
accumulator.write(class_name, def_node.name, :singleton, :public, def_node, toggled: false)
|
|
8456
|
+
end
|
|
8457
|
+
end
|
|
8458
|
+
|
|
8459
|
+
def self_singleton_def?(def_node, qualified_prefix)
|
|
8460
|
+
def_node.receiver.is_a?(Prism::SelfNode) || def_receiver_targets_lexical_self?(def_node.receiver,
|
|
8461
|
+
qualified_prefix)
|
|
8462
|
+
end
|
|
8463
|
+
|
|
8464
|
+
# Issue #1984 — `def Other.x` whose receiver is not the lexical self (`def_owner`) makes `Other.x` public in
|
|
8465
|
+
# whatever order the files load, so each class the constant may name has its singleton slot contested.
|
|
8466
|
+
def contest_foreign_singleton_visibility(def_node, qualified_prefix, def_owner, in_singleton_class, accumulator)
|
|
8467
|
+
receiver = def_node.receiver
|
|
8468
|
+
return unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
8469
|
+
return if !in_singleton_class && def_receiver_targets_lexical_self?(receiver, def_owner)
|
|
8470
|
+
|
|
8471
|
+
constant_receiver_candidates(receiver, qualified_prefix).each do |name|
|
|
8472
|
+
accumulator.contest_slot(name, visibility_slot(def_node.name, :singleton))
|
|
8473
|
+
end
|
|
7924
8474
|
end
|
|
7925
8475
|
|
|
7926
8476
|
# Applies one `private` / `protected` / `public` call ({#visibility_call_parts}) written on the body's self
|
|
7927
|
-
# and returns the running default after it:
|
|
8477
|
+
# and returns the running default after it. `side` is the side its names land on: `:singleton` in a
|
|
8478
|
+
# `class << self` body (issue #1984), `:instance` elsewhere.
|
|
7928
8479
|
#
|
|
7929
8480
|
# - no arguments — the call's visibility becomes the default for subsequent defs.
|
|
7930
8481
|
# - `private :foo, :bar`, `private %i[foo bar]` — back-patch the named methods in the accumulator; the
|
|
7931
8482
|
# default does NOT change for this form.
|
|
7932
8483
|
# - Issues #1911, #1819, #1633 — `private def foo` (endless or not) and `private attr_reader :foo` define
|
|
7933
8484
|
# the members with the call's visibility, whatever the running default is (`private; public def foo` is
|
|
7934
|
-
# public); `private def self.foo` changes the
|
|
7935
|
-
# contested.
|
|
7936
|
-
# - a splat (`private(*names)`) may name any member or none (a bare toggle), so the class's slots
|
|
7937
|
-
# wrote so far are contested, and the statement loop reads the default after it as uncertain
|
|
8485
|
+
# public); `private def self.foo` changes the method `foo` of the side, if one exists, so its slot is
|
|
8486
|
+
# contested, and defines a public singleton `foo` (a singleton-singleton one in a `class << self` body).
|
|
8487
|
+
# - a splat (`private(*names)`) may name any member or none (a bare toggle), so the class's slots on the side
|
|
8488
|
+
# this file wrote so far are contested, and the statement loop reads the default after it as uncertain
|
|
7938
8489
|
# ({#visibility_toggle_arguments?}).
|
|
7939
8490
|
# - any other argument (`private memoize def foo`, `private delegate(...)`) is walked with the default
|
|
7940
8491
|
# uncertain, so a `def` under it is contested: the call's visibility reaches it through a return value
|
|
7941
8492
|
# the walk does not read.
|
|
7942
8493
|
#
|
|
7943
8494
|
# `walk` walks an argument the call does not consume itself.
|
|
7944
|
-
def apply_visibility_call(call_node, name, args, qualified_prefix, current_visibility, accumulator,
|
|
7945
|
-
defs_singleton: false, &walk)
|
|
8495
|
+
def apply_visibility_call(call_node, name, args, qualified_prefix, current_visibility, accumulator, # rubocop:disable Metrics/ParameterLists
|
|
8496
|
+
side: :instance, defs_singleton: false, &walk)
|
|
7946
8497
|
return name if args.empty?
|
|
7947
8498
|
|
|
7948
8499
|
class_name = qualified_prefix.join("::")
|
|
7949
|
-
accumulator.contest_class(class_name) if args.any?(Prism::SplatNode)
|
|
8500
|
+
accumulator.contest_class(class_name, side) if args.any?(Prism::SplatNode)
|
|
7950
8501
|
args.each do |arg|
|
|
7951
8502
|
apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator,
|
|
7952
|
-
defs_singleton: defs_singleton, &walk)
|
|
8503
|
+
side: side, defs_singleton: defs_singleton, &walk)
|
|
7953
8504
|
end
|
|
7954
8505
|
current_visibility
|
|
7955
8506
|
end
|
|
7956
8507
|
|
|
7957
|
-
def apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator, defs_singleton:, &walk)
|
|
8508
|
+
def apply_visibility_argument(arg, name, call_node, qualified_prefix, accumulator, side:, defs_singleton:, &walk)
|
|
7958
8509
|
case arg
|
|
7959
8510
|
when Prism::DefNode
|
|
7960
8511
|
if arg.receiver.nil?
|
|
7961
|
-
|
|
8512
|
+
def_side = side == :instance && defs_singleton ? :eval : side
|
|
8513
|
+
record_def_visibility(arg, qualified_prefix, def_side, name, accumulator, toggled: false)
|
|
7962
8514
|
else
|
|
7963
|
-
accumulator.contest_slot(qualified_prefix.join("::"), arg.name)
|
|
8515
|
+
accumulator.contest_slot(qualified_prefix.join("::"), visibility_slot(arg.name, side))
|
|
8516
|
+
record_def_visibility(arg, qualified_prefix, defs_singleton ? :eval : side, name, accumulator,
|
|
8517
|
+
toggled: false)
|
|
7964
8518
|
end
|
|
7965
8519
|
when Prism::SymbolNode, Prism::StringNode, Prism::ArrayNode
|
|
7966
|
-
apply_named_visibility(arg, name, qualified_prefix, call_node, accumulator, &walk)
|
|
8520
|
+
apply_named_visibility(arg, name, qualified_prefix, call_node, accumulator, side: side, &walk)
|
|
7967
8521
|
else
|
|
7968
8522
|
# An `instance_eval` body's wrapped attr is walked like any other argument and records nothing.
|
|
7969
8523
|
if !defs_singleton && attr_visibility_call?(arg)
|
|
7970
|
-
record_attr_visibility(arg, qualified_prefix, name, accumulator, toggled: false)
|
|
8524
|
+
record_attr_visibility(arg, qualified_prefix, name, accumulator, toggled: false, side: side)
|
|
7971
8525
|
else
|
|
7972
8526
|
accumulator.uncertainly { walk.call(arg) }
|
|
7973
8527
|
end
|
|
@@ -7976,16 +8530,14 @@ module Rigor
|
|
|
7976
8530
|
|
|
7977
8531
|
# `private :foo` / `private "foo"` / `private %i[foo bar]`: each literal name the call's visibility. An array
|
|
7978
8532
|
# element that is not a literal name is walked like any other argument.
|
|
7979
|
-
def apply_named_visibility(arg, visibility, qualified_prefix, call_node, accumulator, &walk)
|
|
8533
|
+
def apply_named_visibility(arg, visibility, qualified_prefix, call_node, accumulator, side: :instance, &walk)
|
|
7980
8534
|
elements = arg.is_a?(Prism::ArrayNode) ? arg.elements : [arg]
|
|
7981
8535
|
class_name = qualified_prefix.join("::")
|
|
7982
8536
|
elements.each do |element|
|
|
7983
8537
|
name = visibility_target_name(element)
|
|
7984
8538
|
next accumulator.uncertainly { walk.call(element) } if name.nil?
|
|
7985
8539
|
|
|
7986
|
-
accumulator
|
|
7987
|
-
accumulator[class_name][name] = visibility
|
|
7988
|
-
accumulator.wrote(class_name, name, call_node, toggled: false)
|
|
8540
|
+
accumulator.write(class_name, name, side, visibility, call_node, toggled: false)
|
|
7989
8541
|
end
|
|
7990
8542
|
end
|
|
7991
8543
|
|
|
@@ -7993,30 +8545,135 @@ module Rigor
|
|
|
7993
8545
|
Source::Literals.symbol_or_string(arg)
|
|
7994
8546
|
end
|
|
7995
8547
|
|
|
8548
|
+
CLASS_METHOD_VISIBILITIES = { private_class_method: :private, public_class_method: :public }.freeze
|
|
8549
|
+
private_constant :CLASS_METHOD_VISIBILITIES
|
|
8550
|
+
|
|
8551
|
+
# Issue #1984 — `[visibility, arguments]` for a `private_class_method` / `public_class_method` call written on
|
|
8552
|
+
# the body's self, in the spellings {#visibility_call_parts} reads (receiverless, `self.`, `send` with a
|
|
8553
|
+
# literal name); nil for any other call.
|
|
8554
|
+
def class_method_visibility_parts(node)
|
|
8555
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
8556
|
+
return nil unless node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
8557
|
+
|
|
8558
|
+
name = node.name
|
|
8559
|
+
arguments = node.arguments&.arguments || EMPTY_VISIBILITY_ARGUMENTS
|
|
8560
|
+
if VISIBILITY_SEND_CALLS.include?(name)
|
|
8561
|
+
name = Source::Literals.symbol_or_string(arguments.first)
|
|
8562
|
+
arguments = arguments.drop(1)
|
|
8563
|
+
end
|
|
8564
|
+
visibility = CLASS_METHOD_VISIBILITIES[name]
|
|
8565
|
+
visibility && [visibility, arguments]
|
|
8566
|
+
end
|
|
8567
|
+
|
|
8568
|
+
# Issue #1984 — applies `private_class_method` / `public_class_method` to the class's singleton slots. It sets
|
|
8569
|
+
# no running default (with no arguments it does nothing). A literal name (`:x`, `"x"`, `[:x]`) and a wrapped
|
|
8570
|
+
# `def self.x` take the call's visibility; a wrapped receiverless `def x` defines an instance method (walked as
|
|
8571
|
+
# a statement) and names the singleton `x` the walk does not see, as does any other receiver, so that slot is
|
|
8572
|
+
# contested; a splat or any other argument may name any singleton method, so every singleton slot the file
|
|
8573
|
+
# wrote so far for the class is contested, and the argument is walked.
|
|
8574
|
+
def apply_class_method_visibility(call_node, visibility, args, qualified_prefix, accumulator, &walk)
|
|
8575
|
+
class_name = qualified_prefix.join("::")
|
|
8576
|
+
args.each do |arg|
|
|
8577
|
+
case arg
|
|
8578
|
+
when Prism::DefNode
|
|
8579
|
+
if arg.receiver && self_singleton_def?(arg, qualified_prefix)
|
|
8580
|
+
accumulator.write(class_name, arg.name, :singleton, visibility, call_node, toggled: false)
|
|
8581
|
+
else
|
|
8582
|
+
walk.call(arg)
|
|
8583
|
+
accumulator.contest_slot(class_name, visibility_slot(arg.name, :singleton))
|
|
8584
|
+
end
|
|
8585
|
+
when Prism::SymbolNode, Prism::StringNode, Prism::ArrayNode
|
|
8586
|
+
apply_named_visibility(arg, visibility, qualified_prefix, call_node, accumulator, side: :singleton) do |el|
|
|
8587
|
+
accumulator.contest_class(class_name, :singleton)
|
|
8588
|
+
walk.call(el)
|
|
8589
|
+
end
|
|
8590
|
+
else
|
|
8591
|
+
accumulator.contest_class(class_name, :singleton)
|
|
8592
|
+
walk.call(arg)
|
|
8593
|
+
end
|
|
8594
|
+
end
|
|
8595
|
+
end
|
|
8596
|
+
|
|
8597
|
+
SINGLETON_DEFINING_CALLS = %i[define_singleton_method].freeze
|
|
8598
|
+
private_constant :SINGLETON_DEFINING_CALLS
|
|
8599
|
+
|
|
8600
|
+
# Issue #1984 — calls in a class body that change a singleton method's visibility in a way the walk does not
|
|
8601
|
+
# read, so the singleton slots they may reach are contested: `define_singleton_method(:x)` (a fresh public
|
|
8602
|
+
# method; a computed name contests every singleton slot of the class), and any call on `singleton_class`
|
|
8603
|
+
# (`singleton_class.send(:public, :x)`, `singleton_class.define_method(:x)`), whose literal arguments name
|
|
8604
|
+
# the slots and whose other arguments may name any.
|
|
8605
|
+
def contest_singleton_rewrites(node, qualified_prefix, accumulator)
|
|
8606
|
+
on_singleton_class = own_singleton_class?(node.receiver)
|
|
8607
|
+
return unless on_singleton_class || node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
8608
|
+
return unless on_singleton_class || SINGLETON_DEFINING_CALLS.include?(node.name)
|
|
8609
|
+
|
|
8610
|
+
class_name = qualified_prefix.join("::")
|
|
8611
|
+
arguments = node.arguments&.arguments || EMPTY_VISIBILITY_ARGUMENTS
|
|
8612
|
+
arguments = arguments.first(1) if SINGLETON_DEFINING_CALLS.include?(node.name)
|
|
8613
|
+
arguments.each do |argument|
|
|
8614
|
+
name = Source::Literals.symbol_or_string(argument)
|
|
8615
|
+
if name
|
|
8616
|
+
accumulator.contest_slot(class_name, visibility_slot(name, :singleton))
|
|
8617
|
+
else
|
|
8618
|
+
accumulator.contest_class(class_name, :singleton)
|
|
8619
|
+
end
|
|
8620
|
+
end
|
|
8621
|
+
end
|
|
8622
|
+
|
|
8623
|
+
# `singleton_class` or `self.singleton_class`: the body's own singleton class.
|
|
8624
|
+
def own_singleton_class?(receiver)
|
|
8625
|
+
singleton_class_of_receiver?(receiver) && (receiver.receiver.nil? || receiver.receiver.is_a?(Prism::SelfNode))
|
|
8626
|
+
end
|
|
8627
|
+
|
|
7996
8628
|
# Issue #1911 — an `attr_reader` / `attr_writer` / `attr_accessor` call written on the body's self records
|
|
7997
8629
|
# each member it defines (`x`, `x=`) with `visibility`: the running default for a statement (`toggled`), the
|
|
7998
8630
|
# wrapping call's for `private attr_accessor :x`, which Ruby applies to the `[:x, :x=]` the macro returns.
|
|
7999
8631
|
# A non-literal name (`attr_reader(*FIELDS)`) records nothing; a call that is no such macro records nothing.
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8632
|
+
# Issue #1989 — the names a `define_method(:x)` or an `alias_method :x, :y` call replaces. An `attr_*` call is
|
|
8633
|
+
# not among them: it writes its members' slots itself ({#record_attr_visibility}).
|
|
8634
|
+
def replaced_by_call(call_node)
|
|
8635
|
+
return define_method_superseded_name(call_node) if call_node.name == :define_method
|
|
8636
|
+
|
|
8637
|
+
Source::AliasNames.alias_method_call_names(call_node)&.first(1) || []
|
|
8638
|
+
end
|
|
8639
|
+
|
|
8640
|
+
# Issue #1989 — a `define_method` or an alias that replaces a method gives the name a visibility the walk
|
|
8641
|
+
# does not read (the running default for `define_method`, the old name's for an alias), so the slot is
|
|
8642
|
+
# contested rather than left asserting the replaced `def`'s. Issue #1984 — the slot is contested whether or
|
|
8643
|
+
# not this file recorded it, since another file may have; issue #2013 — on the instance side too, with the
|
|
8644
|
+
# mark that keeps the contest when that file's write folds later ({VisibilityTable#contest_replaced}).
|
|
8645
|
+
def contest_replaced_visibility(names, qualified_prefix, accumulator, side: :instance)
|
|
8646
|
+
return if names.empty? || qualified_prefix.empty?
|
|
8647
|
+
|
|
8648
|
+
class_name = qualified_prefix.join("::")
|
|
8649
|
+
names.each do |name|
|
|
8650
|
+
if side == :singleton
|
|
8651
|
+
accumulator.contest_slot(class_name, visibility_slot(name, side))
|
|
8652
|
+
else
|
|
8653
|
+
accumulator.contest_replaced(class_name, name)
|
|
8654
|
+
end
|
|
8655
|
+
end
|
|
8656
|
+
end
|
|
8657
|
+
|
|
8658
|
+
def record_attr_visibility(call_node, qualified_prefix, visibility, accumulator, toggled:, side: :instance)
|
|
8659
|
+
macro = attr_macro(call_node)
|
|
8660
|
+
return if macro.nil? || qualified_prefix.empty?
|
|
8003
8661
|
|
|
8004
8662
|
class_name = qualified_prefix.join("::")
|
|
8005
8663
|
each_attr_macro_name(call_node) do |base|
|
|
8006
8664
|
members = []
|
|
8007
|
-
members << base unless
|
|
8008
|
-
members << :"#{base}=" unless
|
|
8665
|
+
members << base unless macro == :attr_writer
|
|
8666
|
+
members << :"#{base}=" unless macro == :attr_reader
|
|
8009
8667
|
members.each do |member|
|
|
8010
|
-
accumulator
|
|
8011
|
-
accumulator[class_name][member] = visibility
|
|
8012
|
-
accumulator.wrote(class_name, member, call_node, toggled: toggled)
|
|
8668
|
+
accumulator.write(class_name, member, side, visibility, call_node, toggled: toggled)
|
|
8013
8669
|
end
|
|
8014
8670
|
end
|
|
8015
8671
|
end
|
|
8016
8672
|
|
|
8017
|
-
#
|
|
8673
|
+
# An `attr_reader` / `attr_writer` / `attr_accessor` call on the body's self ({#attr_macro},
|
|
8674
|
+
# {#record_attr_visibility}). Ruby 4.0.5 gives every spelling the running default visibility.
|
|
8018
8675
|
def attr_visibility_call?(node)
|
|
8019
|
-
node
|
|
8676
|
+
!attr_macro(node).nil?
|
|
8020
8677
|
end
|
|
8021
8678
|
|
|
8022
8679
|
# Registers the alias name in the `discovered_methods` table so `undefined-method` diagnostics are not emitted for
|
|
@@ -8131,7 +8788,8 @@ module Rigor
|
|
|
8131
8788
|
return if owner.nil?
|
|
8132
8789
|
|
|
8133
8790
|
singleton_class_rewritten_names(node).each do |method_name|
|
|
8134
|
-
|
|
8791
|
+
# `attr_writer :x` defines `x=` alone (#1936), so `x` keeps its own `def`.
|
|
8792
|
+
names = node.name == :attr_writer ? [] : [method_name]
|
|
8135
8793
|
names << :"#{method_name}=" if %i[attr_writer attr_accessor].include?(node.name)
|
|
8136
8794
|
names.each do |name|
|
|
8137
8795
|
record_envelope(tables.envelopes, owner, [:singleton, name], Source::ParameterEnvelope::OPAQUE)
|
|
@@ -8234,48 +8892,76 @@ module Rigor
|
|
|
8234
8892
|
#
|
|
8235
8893
|
# ADR-119 WD3 — the alias is the slot's writer: the new name's slot is contested when the alias is possible
|
|
8236
8894
|
# or the slot it copies is contested, and cleared otherwise.
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8895
|
+
#
|
|
8896
|
+
# Issue #2006 — in source order. An alias copies what its old name held AT the alias, read from the walk's
|
|
8897
|
+
# definer history ({MethodTables#wrote_definer}) and the copies earlier aliases made: a `def` of the old name
|
|
8898
|
+
# written after the alias (`alias old_x x; def x = 1`) is not what `old_x` runs, and nothing before the alias
|
|
8899
|
+
# copies nothing. The new name's slot then follows its last writer: a later `def` keeps the slot the walk
|
|
8900
|
+
# gave it, and a later `define_method` / `attr_*` leaves the copy contested, as {#supersede_def_nodes} leaves
|
|
8901
|
+
# a replaced `def`.
|
|
8902
|
+
def apply_alias_def_nodes(root, accumulator, tables)
|
|
8903
|
+
alias_map = collect_class_alias_map(root, [], AliasMap.new.track(tables.certainty))
|
|
8904
|
+
history = (tables.definer_history || {}).transform_values(&:dup)
|
|
8905
|
+
alias_map.copy_into(history).each { |key| settle_alias_slot(accumulator, tables, key, history[key]) }
|
|
8906
|
+
end
|
|
8907
|
+
|
|
8908
|
+
# Writes the new name's slot from its last writer `writes.last` ({#apply_alias_def_nodes}): an alias copy or a
|
|
8909
|
+
# later `def` writes its `def` with its certainty (for the `def`, what the walk wrote already). A last writer
|
|
8910
|
+
# with no `def` this file holds — a later `define_method` / `attr_*`, or an alias that copies nothing this file
|
|
8911
|
+
# defines (an inherited or core method, an attr reader, a name it cannot read) — contests the slot, which keeps
|
|
8912
|
+
# what it holds or else the latest copy, so no sibling file's `def` fills it.
|
|
8913
|
+
def settle_alias_slot(accumulator, tables, (class_name, new_name), writes)
|
|
8914
|
+
last = writes.last
|
|
8915
|
+
slot = (accumulator[class_name] ||= {})
|
|
8916
|
+
if last[1]
|
|
8917
|
+
slot[new_name] = last[1]
|
|
8918
|
+
follow_slot_writer(tables, :contested_def_nodes, [class_name, new_name], last[2])
|
|
8919
|
+
else
|
|
8920
|
+
copy = writes.rfind { |write| write[1] }
|
|
8921
|
+
slot[new_name] ||= copy[1] if copy
|
|
8922
|
+
follow_slot_writer(tables, :contested_def_nodes, [class_name, new_name], false)
|
|
8254
8923
|
end
|
|
8255
8924
|
end
|
|
8256
8925
|
|
|
8257
|
-
# The `{class => {new name => old name}}` map {#collect_class_alias_map} fills, which also
|
|
8258
|
-
#
|
|
8926
|
+
# The `{class => {new name => old name}}` map {#collect_class_alias_map} fills, which also lists every alias it
|
|
8927
|
+
# met as an event `[class, new name, old name, start offset, possible?]` ({Certainty}).
|
|
8259
8928
|
class AliasMap < Hash
|
|
8929
|
+
attr_reader :events
|
|
8930
|
+
|
|
8260
8931
|
def track(certainty)
|
|
8261
8932
|
@certainty = certainty || Certainty::EMPTY
|
|
8262
|
-
@
|
|
8933
|
+
@events = []
|
|
8263
8934
|
self
|
|
8264
8935
|
end
|
|
8265
8936
|
|
|
8266
|
-
# Records that `node` wrote the `[class_name, new_name]` entry
|
|
8267
|
-
def wrote(class_name, new_name, node)
|
|
8937
|
+
# Records that `node` wrote the `[class_name, new_name]` entry, aliasing `old_name`.
|
|
8938
|
+
def wrote(class_name, new_name, old_name, node)
|
|
8268
8939
|
return if @certainty.nil?
|
|
8269
8940
|
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8941
|
+
@events << [class_name, new_name, old_name, node.location.start_offset, Certainty.possible?(@certainty, node)]
|
|
8942
|
+
end
|
|
8943
|
+
|
|
8944
|
+
# Issue #2006 — adds every alias to `history` (`{[class, name] => [[offset, def node or nil, certain,
|
|
8945
|
+
# (:alias)], ...]}`, ascending), in source order. An alias whose old name held a `def` at the alias copies it,
|
|
8946
|
+
# certain when that write and the alias are. Any other alias — an old name with no `def` of this file before
|
|
8947
|
+
# it (inherited, core, an attr reader, a sibling file's), or one it cannot read — adds a write with no `def`,
|
|
8948
|
+
# which leaves the slot contested ({ScopeIndexer#settle_alias_slot}). An alias whose new name it cannot read
|
|
8949
|
+
# adds one to every name of the class written before it. Returns the `[class, name]` keys it wrote.
|
|
8950
|
+
def copy_into(history)
|
|
8951
|
+
@events.sort_by { |event| event[3] }.flat_map do |class_name, new_name, old_name, at, possible|
|
|
8952
|
+
source = old_name && history[[class_name, old_name]]&.rfind { |write| write.first < at }
|
|
8953
|
+
write = if source&.[](1) then [at, source[1], source[2] && !possible, :alias].freeze
|
|
8954
|
+
else [at, nil, false, :alias].freeze
|
|
8955
|
+
end
|
|
8956
|
+
keys = new_name ? [[class_name, new_name]] : names_before(history, class_name, at)
|
|
8957
|
+
keys.each { |key| (history[key] ||= []).push(write).sort_by!(&:first) }
|
|
8958
|
+
end.uniq
|
|
8275
8959
|
end
|
|
8276
8960
|
|
|
8277
|
-
|
|
8278
|
-
|
|
8961
|
+
private
|
|
8962
|
+
|
|
8963
|
+
def names_before(history, class_name, at)
|
|
8964
|
+
history.filter_map { |key, writes| key if key.first == class_name && writes.first.first < at }
|
|
8279
8965
|
end
|
|
8280
8966
|
end
|
|
8281
8967
|
|
|
@@ -8337,12 +9023,19 @@ module Rigor
|
|
|
8337
9023
|
# `t` alias was the corpus case). Unlike AliasMethodNode a call's children can carry further
|
|
8338
9024
|
# class bodies (`Class.new do … end`), so the walk continues below it either way.
|
|
8339
9025
|
names = Source::AliasNames.alias_method_call_names(node)
|
|
8340
|
-
if names && !rec_prefix.empty? && !(singleton_cref && leaf_owner.nil?)
|
|
9026
|
+
if (names || unread_alias_method?(node)) && !rec_prefix.empty? && !(singleton_cref && leaf_owner.nil?)
|
|
8341
9027
|
record_alias_call_entry(accumulator, rec_prefix.join("::"), names, node)
|
|
8342
9028
|
end
|
|
8343
9029
|
false
|
|
8344
9030
|
end
|
|
8345
9031
|
|
|
9032
|
+
# Issue #2006 — an `alias_method` call on no receiver or `self` whose names {Source::AliasNames} cannot read
|
|
9033
|
+
# (`alias_method n, :to_s`, `self.alias_method :x, :y`, a splat): it still replaces a method, which
|
|
9034
|
+
# {AliasMap#copy_into} contests.
|
|
9035
|
+
def unread_alias_method?(node)
|
|
9036
|
+
node.name == :alias_method && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
9037
|
+
end
|
|
9038
|
+
|
|
8346
9039
|
# The rebinding-block call arm of {#collect_class_alias_map}: an anonymous meta-new call
|
|
8347
9040
|
# (`Class.new { … }` no write names) files its block's aliases nowhere, and an eval-family
|
|
8348
9041
|
# call binds them on the receiver — a nameable receiver re-anchors the owner while a
|
|
@@ -8433,20 +9126,32 @@ module Rigor
|
|
|
8433
9126
|
end
|
|
8434
9127
|
|
|
8435
9128
|
def record_alias_call_entry(accumulator, class_name, names, node)
|
|
9129
|
+
if names.nil?
|
|
9130
|
+
# Issue #2006 — an unread `alias_method` maps nothing, but still replaces a method ({AliasMap#copy_into}).
|
|
9131
|
+
new_name = Source::AliasNames.literal_name(node.arguments&.arguments&.first)
|
|
9132
|
+
accumulator.wrote(class_name, new_name, nil, node) if accumulator.is_a?(AliasMap)
|
|
9133
|
+
return
|
|
9134
|
+
end
|
|
9135
|
+
|
|
8436
9136
|
(accumulator[class_name] ||= {})[names.first] = names.last
|
|
8437
|
-
accumulator.wrote(class_name, names.first, node) if accumulator.is_a?(AliasMap)
|
|
9137
|
+
accumulator.wrote(class_name, names.first, names.last, node) if accumulator.is_a?(AliasMap)
|
|
8438
9138
|
end
|
|
8439
9139
|
|
|
8440
9140
|
def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
|
|
8441
9141
|
return if qualified_prefix.empty?
|
|
8442
9142
|
|
|
8443
9143
|
names = Source::AliasNames.keyword_names(alias_node)
|
|
8444
|
-
return if names.nil?
|
|
8445
|
-
|
|
8446
9144
|
class_name = qualified_prefix.join("::")
|
|
9145
|
+
if names.nil?
|
|
9146
|
+
# Issue #2006 — an `alias` whose names are not plain symbols still replaces a method ({AliasMap#copy_into}).
|
|
9147
|
+
new_name = Source::AliasNames.symbol_name(alias_node.new_name)
|
|
9148
|
+
accumulator.wrote(class_name, new_name, nil, alias_node) if accumulator.is_a?(AliasMap)
|
|
9149
|
+
return
|
|
9150
|
+
end
|
|
9151
|
+
|
|
8447
9152
|
new_name, old_name = names
|
|
8448
9153
|
(accumulator[class_name] ||= {})[new_name] = old_name
|
|
8449
|
-
accumulator.wrote(class_name, new_name, alias_node) if accumulator.is_a?(AliasMap)
|
|
9154
|
+
accumulator.wrote(class_name, new_name, old_name, alias_node) if accumulator.is_a?(AliasMap)
|
|
8450
9155
|
end
|
|
8451
9156
|
|
|
8452
9157
|
def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator)
|
|
@@ -8474,8 +9179,9 @@ module Rigor
|
|
|
8474
9179
|
return if qualified_prefix.empty?
|
|
8475
9180
|
|
|
8476
9181
|
kind = in_singleton_class ? :singleton : :instance
|
|
8477
|
-
|
|
8478
|
-
|
|
9182
|
+
macro = attr_macro(call_node)
|
|
9183
|
+
reader = macro != :attr_writer
|
|
9184
|
+
writer = macro != :attr_reader
|
|
8479
9185
|
class_name = qualified_prefix.join("::")
|
|
8480
9186
|
each_attr_macro_name(call_node) do |base|
|
|
8481
9187
|
record_method(accumulator, class_name, base, kind) if reader
|
|
@@ -8483,18 +9189,34 @@ module Rigor
|
|
|
8483
9189
|
end
|
|
8484
9190
|
end
|
|
8485
9191
|
|
|
8486
|
-
# Yields each name an `attr_*` call declares: a Symbol or String literal argument. A
|
|
8487
|
-
# (`attr_writer(*names)`) names nothing Rigor can read.
|
|
9192
|
+
# Yields each name an `attr_*` call declares ({#attr_macro}): a Symbol or String literal argument. A
|
|
9193
|
+
# non-literal one (`attr_writer(*names)`, `send(:attr_writer, name)`) names nothing Rigor can read.
|
|
8488
9194
|
def each_attr_macro_name(call_node)
|
|
8489
|
-
return
|
|
8490
|
-
return if call_node.arguments.nil?
|
|
9195
|
+
return if attr_macro(call_node).nil? || call_node.arguments.nil?
|
|
8491
9196
|
|
|
8492
|
-
call_node.arguments.arguments
|
|
9197
|
+
arguments = call_node.arguments.arguments
|
|
9198
|
+
arguments = arguments.drop(1) if SURFACE_SEND_CALLS.include?(call_node.name)
|
|
9199
|
+
arguments.each do |arg|
|
|
8493
9200
|
base = literal_method_name(arg)
|
|
8494
9201
|
yield base unless base.nil?
|
|
8495
9202
|
end
|
|
8496
9203
|
end
|
|
8497
9204
|
|
|
9205
|
+
# Issue #2097 — the {ATTR_MACROS} entry a call declares members with on the lexical class, or nil. The
|
|
9206
|
+
# call is on the body's self, implicitly or as `self.` (`self.attr_accessor :x`), and names the macro
|
|
9207
|
+
# directly or as the literal first argument of `send` / `public_send` / `__send__`
|
|
9208
|
+
# (`send(:attr_accessor, :x)`); the same spellings `SigGen::Generator#attr_writer_names` reads. Any
|
|
9209
|
+
# other receiver defines on another object.
|
|
9210
|
+
def attr_macro(call_node)
|
|
9211
|
+
return nil unless call_node.is_a?(Prism::CallNode)
|
|
9212
|
+
return nil unless call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
|
|
9213
|
+
return call_node.name if ATTR_MACROS.include?(call_node.name)
|
|
9214
|
+
return nil unless SURFACE_SEND_CALLS.include?(call_node.name)
|
|
9215
|
+
|
|
9216
|
+
macro = literal_method_name(call_node.arguments&.arguments&.first)
|
|
9217
|
+
macro if ATTR_MACROS.include?(macro)
|
|
9218
|
+
end
|
|
9219
|
+
|
|
8498
9220
|
# Issue #736 — the module-level accessor macros, which introduce methods on BOTH sides of the class.
|
|
8499
9221
|
# ActiveSupport defines them on `Module`, so every class and module body can use them, and Rails codebases
|
|
8500
9222
|
# do: `mattr_accessor` / `cattr_accessor` (an alias of it) and `class_attribute` account for 92 false
|
|
@@ -8616,14 +9338,15 @@ module Rigor
|
|
|
8616
9338
|
# {#discovered_project_index_incremental}'s changed-file branch) yields the live def nodes the signature
|
|
8617
9339
|
# reads their parameter structure from. Issue #1120 — and each file's own refinement table, which the merged
|
|
8618
9340
|
# def-index cannot attribute back to a file, so the session can diff it against the file's seed bundle.
|
|
8619
|
-
# Issue #1459 — and each file's {#defined_method_names}, diffed the same way.
|
|
8620
|
-
#
|
|
9341
|
+
# Issue #1459 — and each file's {#defined_method_names}, diffed the same way. Issue #2134 — and each file's
|
|
9342
|
+
# {#visibility_entries}. Issue #2158 — and each file's `global_write_census`.
|
|
9343
|
+
# @return `{ def_index:, code_fingerprints:, declaration_signatures:, refinements:, defined_method_names:,
|
|
9344
|
+
# visibility_entries:, global_write_census: }`.
|
|
8621
9345
|
def scan_summary_for_paths(paths, buffer: nil)
|
|
8622
9346
|
acc = new_def_index_accumulator
|
|
8623
9347
|
code_fingerprints = {}
|
|
8624
9348
|
declaration_signatures = {}
|
|
8625
|
-
|
|
8626
|
-
defined_method_names = {}
|
|
9349
|
+
per_file = { refinements: {}, defined_method_names: {}, visibility_entries: {}, global_write_census: {} }
|
|
8627
9350
|
paths.each do |path|
|
|
8628
9351
|
physical = buffer ? buffer.resolve(path) : path
|
|
8629
9352
|
source = File.read(physical)
|
|
@@ -8632,16 +9355,21 @@ module Rigor
|
|
|
8632
9355
|
fold_file_index(acc, file_index)
|
|
8633
9356
|
code_fingerprints[path] = code_fingerprint(source, parsed.comments)
|
|
8634
9357
|
declaration_signatures[path] = declaration_signature(file_index)
|
|
8635
|
-
|
|
8636
|
-
defined_method_names[path] = defined_method_names(file_index)
|
|
9358
|
+
file_summary(file_index).each { |key, table| per_file[key][path] = table unless table.nil? }
|
|
8637
9359
|
rescue DeclarationWalk::ContractError
|
|
8638
9360
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
8639
9361
|
rescue StandardError
|
|
8640
9362
|
next
|
|
8641
9363
|
end
|
|
8642
9364
|
{ def_index: finalize_def_index(acc), code_fingerprints: code_fingerprints,
|
|
8643
|
-
declaration_signatures: declaration_signatures,
|
|
8644
|
-
|
|
9365
|
+
declaration_signatures: declaration_signatures, **per_file }
|
|
9366
|
+
end
|
|
9367
|
+
|
|
9368
|
+
# One file's tables {#scan_summary_for_paths} hands the session to diff against the file's seed bundle (nil for
|
|
9369
|
+
# a table the file does not have).
|
|
9370
|
+
def file_summary(file_index)
|
|
9371
|
+
{ refinements: file_index[:refinements], defined_method_names: defined_method_names(file_index),
|
|
9372
|
+
visibility_entries: visibility_entries(file_index), global_write_census: file_index[:global_write_census] }
|
|
8645
9373
|
end
|
|
8646
9374
|
|
|
8647
9375
|
# Issue #1459 — every method name one file's index defines, on any class or module and either side: the keys of
|
|
@@ -8655,9 +9383,39 @@ module Rigor
|
|
|
8655
9383
|
%i[methods def_nodes singleton_def_nodes].each do |key|
|
|
8656
9384
|
file_index[key]&.each_value { |table| names.merge(table.keys) }
|
|
8657
9385
|
end
|
|
9386
|
+
# Issue #2184 — a named deferred-range row with no side (a `def` in a block, a `define_singleton_method`) is a
|
|
9387
|
+
# definition no table above holds.
|
|
9388
|
+
file_index[:deferred_ranges]&.each_value { |rows| rows.each { |row| names << row[2] if row[2] && row[3].nil? } }
|
|
8658
9389
|
names.freeze
|
|
8659
9390
|
end
|
|
8660
9391
|
|
|
9392
|
+
# Issue #2134 — one file's contributions to the visibility table and its contested sibling, as a Set of
|
|
9393
|
+
# `[class, slot, visibility]` entries and `[:contested, class, slot]` contests, where a slot is a method name or
|
|
9394
|
+
# `[:singleton, name]`. Takes a live file index or a seed bundle, which hold the same keys.
|
|
9395
|
+
#
|
|
9396
|
+
# Issue #2161 — and a `[:module_function, owner, name]` entry for each of the file's deferred-range rows that
|
|
9397
|
+
# `DefinerResolution.module_function_row?` counts (a named def row whose kind is not `:instance`): a module
|
|
9398
|
+
# function's singleton copy is public whatever its instance slot says, so the row is a visibility fact the
|
|
9399
|
+
# table itself does not hold.
|
|
9400
|
+
def visibility_entries(file_index)
|
|
9401
|
+
entries = Set.new
|
|
9402
|
+
file_index[:method_visibilities]&.each do |class_name, table|
|
|
9403
|
+
table.each { |slot, visibility| entries << [class_name, slot, visibility] }
|
|
9404
|
+
end
|
|
9405
|
+
contested = (file_index[:siblings] || {})[:contested_discovered_method_visibilities]
|
|
9406
|
+
contested&.each { |(class_name, slot)| entries << [:contested, class_name, slot] }
|
|
9407
|
+
add_module_function_entries(file_index[:deferred_ranges], entries)
|
|
9408
|
+
entries.freeze
|
|
9409
|
+
end
|
|
9410
|
+
|
|
9411
|
+
def add_module_function_entries(deferred_ranges, entries)
|
|
9412
|
+
deferred_ranges&.each_value do |rows|
|
|
9413
|
+
rows.each do |(_start, _finish, name, kind, owner)|
|
|
9414
|
+
entries << [:module_function, owner, name] if name && owner && kind != :instance
|
|
9415
|
+
end
|
|
9416
|
+
end
|
|
9417
|
+
end
|
|
9418
|
+
|
|
8661
9419
|
# ADR-89 WD1 — a per-file digest of every cross-file DECLARATION surface an ancestry / file-level
|
|
8662
9420
|
# dependent consumes, EXCLUDING method bodies (which only symbol dependents consume, via the ADR-46
|
|
8663
9421
|
# symbol edges the change-detection already fingerprints per method). A body edit that touches no
|
|
@@ -8875,14 +9633,21 @@ module Rigor
|
|
|
8875
9633
|
# Error degradation is identical to the two independent loops it replaces: a read / parse failure (the
|
|
8876
9634
|
# rescue's real target) contributes nothing to either table. The subset-scoped callers
|
|
8877
9635
|
# ({IncrementalSession}, `coverage --protection`) keep calling the individual methods unchanged.
|
|
8878
|
-
|
|
9636
|
+
#
|
|
9637
|
+
# @param uncertain — issue #2021: the paths whose every method contribution is `possible`
|
|
9638
|
+
# ({Certainty.every_contribution}).
|
|
9639
|
+
def discovered_project_index_for_paths(paths, buffer: nil, uncertain: EMPTY_PATH_SET)
|
|
8879
9640
|
classes = {}
|
|
8880
9641
|
acc = new_def_index_accumulator
|
|
8881
9642
|
paths.each do |path|
|
|
8882
9643
|
physical = buffer ? buffer.resolve(path) : path
|
|
8883
9644
|
root = Prism.parse(File.read(physical), filepath: path).value
|
|
8884
9645
|
collect_class_decls(root, [], classes)
|
|
8885
|
-
|
|
9646
|
+
if uncertain.include?(path)
|
|
9647
|
+
fold_file_index(acc, without_written_slots(build_file_index(path, root, uncertain: true), acc))
|
|
9648
|
+
else
|
|
9649
|
+
accumulate_project_index(acc, path, root)
|
|
9650
|
+
end
|
|
8886
9651
|
rescue DeclarationWalk::ContractError
|
|
8887
9652
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
8888
9653
|
rescue StandardError
|
|
@@ -8930,14 +9695,19 @@ module Rigor
|
|
|
8930
9695
|
# @param paths — project file paths, in canonical order.
|
|
8931
9696
|
# @param seed_bundles — the prior run's per-file bundles, keyed by logical path.
|
|
8932
9697
|
# @return `{ classes:, def_index:, bundles: }`.
|
|
8933
|
-
|
|
9698
|
+
# @param uncertain — issue #2021: as {#discovered_project_index_for_paths}. Such a file is always re-walked and
|
|
9699
|
+
# keeps no bundle, since a bundle does not record that its contributions were all possible.
|
|
9700
|
+
def discovered_project_index_incremental(paths, seed_bundles:, buffer: nil, uncertain: EMPTY_PATH_SET) # rubocop:disable Metrics/AbcSize
|
|
8934
9701
|
classes = {}
|
|
8935
9702
|
acc = new_def_index_accumulator
|
|
8936
9703
|
bundles = {}
|
|
8937
9704
|
paths.each do |path|
|
|
8938
9705
|
physical = buffer ? buffer.resolve(path) : path
|
|
8939
9706
|
digest = Cache::FileDigest.hexdigest(physical)
|
|
8940
|
-
|
|
9707
|
+
walked = uncertain.include?(path)
|
|
9708
|
+
# A walked file neither reuses nor stores a bundle. Both are guards no spec separates: no bundle is ever
|
|
9709
|
+
# stored for a walked file, so the reuse guard matters only if a path changes role within one session.
|
|
9710
|
+
cached = walked ? nil : seed_bundles[path]
|
|
8941
9711
|
if cached && cached[:digest] == digest
|
|
8942
9712
|
bundles[path] = cached
|
|
8943
9713
|
classes.merge!(cached[:classes])
|
|
@@ -8946,13 +9716,13 @@ module Rigor
|
|
|
8946
9716
|
source = File.read(physical)
|
|
8947
9717
|
parsed = Prism.parse(source, filepath: path)
|
|
8948
9718
|
root = parsed.value
|
|
8949
|
-
file_index = build_file_index(path, root)
|
|
9719
|
+
file_index = build_file_index(path, root, uncertain: walked)
|
|
8950
9720
|
file_classes = {}
|
|
8951
9721
|
collect_class_decls(root, [], file_classes)
|
|
8952
|
-
|
|
8953
|
-
|
|
9722
|
+
fingerprint = code_fingerprint(source, parsed.comments)
|
|
9723
|
+
bundles[path] = build_seed_bundle(file_index, file_classes, digest, fingerprint) unless walked
|
|
8954
9724
|
classes.merge!(file_classes)
|
|
8955
|
-
fold_file_index(acc, file_index)
|
|
9725
|
+
fold_file_index(acc, walked ? without_written_slots(file_index, acc) : file_index)
|
|
8956
9726
|
end
|
|
8957
9727
|
rescue DeclarationWalk::ContractError
|
|
8958
9728
|
raise # a broken walk contract, not an unreadable file: skipping it would hide the failure
|
|
@@ -8966,12 +9736,43 @@ module Rigor
|
|
|
8966
9736
|
# Builds ONE file's isolated def-index contribution (live `Prism::DefNode`s) by folding it into a fresh
|
|
8967
9737
|
# accumulator — recon Q2(a)'s "constructible in isolation" property. Shared by the cold-baseline bundle
|
|
8968
9738
|
# build and the incremental changed-file re-walk.
|
|
8969
|
-
def build_file_index(path, root)
|
|
9739
|
+
def build_file_index(path, root, uncertain: false)
|
|
8970
9740
|
file_acc = new_def_index_accumulator
|
|
8971
|
-
accumulate_project_index(file_acc, path, root)
|
|
9741
|
+
accumulate_project_index(file_acc, path, root, uncertain: uncertain)
|
|
8972
9742
|
file_acc
|
|
8973
9743
|
end
|
|
8974
9744
|
|
|
9745
|
+
# Issue #2021 — the single-valued slot tables an `uncertain` file writes, each with its contest sibling.
|
|
9746
|
+
UNCERTAIN_SLOT_TABLES = {
|
|
9747
|
+
def_nodes: :contested_discovered_def_nodes,
|
|
9748
|
+
singleton_def_nodes: :contested_discovered_singleton_def_nodes,
|
|
9749
|
+
method_visibilities: :contested_discovered_method_visibilities,
|
|
9750
|
+
parameter_envelopes: :contested_discovered_parameter_envelopes
|
|
9751
|
+
}.freeze
|
|
9752
|
+
|
|
9753
|
+
# Issue #2021 — an `uncertain` file's isolated index without the slots `acc` already holds. The walked files fold
|
|
9754
|
+
# after every other discovered file, so `acc` holds what the configured (certain) files wrote: a walked reopening
|
|
9755
|
+
# of such a class adds the names it defines (existence) and leaves each written def node, visibility and
|
|
9756
|
+
# envelope, and its contest, as the certain writer left it. Without this, the walked file's possible write was
|
|
9757
|
+
# the slot's last and contested it, and `rigor check lib/user.rb plug/x.rb` withheld what `rigor check
|
|
9758
|
+
# lib/user.rb` reports. A slot no earlier file wrote is taken, contested.
|
|
9759
|
+
def without_written_slots(file_index, acc)
|
|
9760
|
+
siblings = (file_index[:siblings] || {}).dup
|
|
9761
|
+
tables = UNCERTAIN_SLOT_TABLES.to_h do |table, sibling|
|
|
9762
|
+
written = acc[table]
|
|
9763
|
+
kept = {}
|
|
9764
|
+
(file_index[table] || {}).each do |class_name, entries|
|
|
9765
|
+
sitting = written[class_name]
|
|
9766
|
+
entries = entries.reject { |key, _| sitting.key?(key) } if sitting
|
|
9767
|
+
kept[class_name] = entries unless entries.empty?
|
|
9768
|
+
end
|
|
9769
|
+
contests = siblings[sibling]
|
|
9770
|
+
siblings[sibling] = contests.select { |class_name, key| kept[class_name]&.key?(key) }.to_set if contests
|
|
9771
|
+
[table, kept]
|
|
9772
|
+
end
|
|
9773
|
+
file_index.merge(tables).merge(siblings: siblings)
|
|
9774
|
+
end
|
|
9775
|
+
|
|
8975
9776
|
# ADR-85 WD2 — folds a single file's isolated def-index contribution into the cross-file accumulator,
|
|
8976
9777
|
# applying EXACTLY the merge semantics {#accumulate_project_index} applies incrementally (def_nodes
|
|
8977
9778
|
# later-wins, def_sources first-wins, includes / class_sources accumulate, everything else later-wins).
|
|
@@ -9031,7 +9832,7 @@ module Rigor
|
|
|
9031
9832
|
when :contested_discovered_singleton_def_nodes
|
|
9032
9833
|
fold_slot_contests(target.fetch(name), singleton_def_nodes, table)
|
|
9033
9834
|
when :contested_discovered_method_visibilities
|
|
9034
|
-
|
|
9835
|
+
fold_visibility_contests(target.fetch(name), acc[:method_visibilities], method_visibilities, table)
|
|
9035
9836
|
else union_sibling_value(target.fetch(name), table)
|
|
9036
9837
|
end
|
|
9037
9838
|
end
|
|
@@ -9104,6 +9905,35 @@ module Rigor
|
|
|
9104
9905
|
result.merge(file_contested)
|
|
9105
9906
|
end
|
|
9106
9907
|
|
|
9908
|
+
# The visibility table's {#fold_slot_contests}, where `recorded` is the table the file folds into as it stood.
|
|
9909
|
+
# Issue #1984 — a singleton slot (`[:singleton, name]`) does not follow its writer: file order is not load
|
|
9910
|
+
# order, so two files that give it different visibilities (`private_class_method :x` here,
|
|
9911
|
+
# `class << self; public :x; end` in a reopening) leave it contested, and a contest on it stays whichever
|
|
9912
|
+
# file folds later. The instance slots fold as before, except (issue #2013) one whose contest carries a
|
|
9913
|
+
# replacement's mark ({ScopeIndexer.replaced_visibility_mark}): a `define_method` or an alias in one file and a
|
|
9914
|
+
# `private def` in another leave the visibility to load order, so that contest stays too.
|
|
9915
|
+
def fold_visibility_contests(sitting, recorded, file_slots, file_contested)
|
|
9916
|
+
stale = sitting.select do |class_name, name|
|
|
9917
|
+
!name.is_a?(Array) && file_slots[class_name]&.key?(name) &&
|
|
9918
|
+
!sitting.include?([class_name, ScopeIndexer.replaced_visibility_mark(name)])
|
|
9919
|
+
end
|
|
9920
|
+
disagreeing = singleton_visibility_disagreements(recorded, file_slots)
|
|
9921
|
+
return sitting if stale.empty? && file_contested.empty? && disagreeing.empty?
|
|
9922
|
+
|
|
9923
|
+
(sitting - stale).merge(file_contested).merge(disagreeing)
|
|
9924
|
+
end
|
|
9925
|
+
|
|
9926
|
+
def singleton_visibility_disagreements(recorded, file_slots)
|
|
9927
|
+
file_slots.flat_map do |class_name, table|
|
|
9928
|
+
sitting = recorded[class_name]
|
|
9929
|
+
next [] if sitting.nil?
|
|
9930
|
+
|
|
9931
|
+
table.filter_map do |slot, visibility|
|
|
9932
|
+
[class_name, slot] if slot.is_a?(Array) && sitting.key?(slot) && sitting[slot] != visibility
|
|
9933
|
+
end
|
|
9934
|
+
end
|
|
9935
|
+
end
|
|
9936
|
+
|
|
9107
9937
|
def union_paths(sitting, arriving)
|
|
9108
9938
|
return sitting if arriving.empty?
|
|
9109
9939
|
return arriving if sitting.empty?
|
|
@@ -9509,12 +10339,12 @@ module Rigor
|
|
|
9509
10339
|
# Folds one file's class-keyed indexes into the cross-file accumulator. `method_visibilities` (ADR-35) is
|
|
9510
10340
|
# collected here so the override-visibility-reduced rule can read an ancestor's visibility declared in a sibling
|
|
9511
10341
|
# file.
|
|
9512
|
-
def accumulate_project_index(acc, path, root)
|
|
10342
|
+
def accumulate_project_index(acc, path, root, uncertain: false)
|
|
9513
10343
|
# One combined descent yields both the methods existence table and the def-node table; the latter is also
|
|
9514
10344
|
# consumed by `record_class_sources`, so a def-dense file is walked once here instead of three times (methods +
|
|
9515
10345
|
# def-nodes ×2). See {#build_methods_and_def_nodes}.
|
|
9516
10346
|
file_methods, file_def_nodes, file_envelopes, file_refinements, file_singleton_def_nodes, certainty =
|
|
9517
|
-
walk_def_contributions(acc, path, root)
|
|
10347
|
+
walk_def_contributions(acc, path, root, uncertain: uncertain)
|
|
9518
10348
|
merge_discovered_defs(acc[:def_nodes], acc[:def_sources], path, file_def_nodes)
|
|
9519
10349
|
fold_parameter_envelopes(acc, file_envelopes)
|
|
9520
10350
|
fold_refinements(acc, file_refinements)
|
|
@@ -9541,8 +10371,8 @@ module Rigor
|
|
|
9541
10371
|
# file's siblings folded into the accumulator BEFORE its members join it (ADR-119 WD3: the `possible` rule
|
|
9542
10372
|
# reads the accumulator's member as it stood). Returns `[methods, def_nodes, envelopes, refinements,
|
|
9543
10373
|
# singleton_def_nodes, certainty]`, the classification passed on for the visibility walk.
|
|
9544
|
-
def walk_def_contributions(acc, path, root)
|
|
9545
|
-
certainty = Certainty.possible_nodes(root)
|
|
10374
|
+
def walk_def_contributions(acc, path, root, uncertain: false)
|
|
10375
|
+
certainty = uncertain ? Certainty.every_contribution(root) : Certainty.possible_nodes(root)
|
|
9546
10376
|
file_methods, file_def_nodes, file_envelopes, file_refinements, file_siblings =
|
|
9547
10377
|
build_methods_and_def_nodes(root, path, certainty: certainty)
|
|
9548
10378
|
file_singleton_def_nodes, file_singleton_contested = build_singleton_def_tables(root, certainty: certainty)
|
|
@@ -9588,7 +10418,15 @@ module Rigor
|
|
|
9588
10418
|
census.writes.each do |name, descriptor|
|
|
9589
10419
|
(acc[:constant_writes][name] ||= {})[path] = descriptor
|
|
9590
10420
|
end
|
|
9591
|
-
acc[:global_write_census].merge(census.write_census.census)
|
|
10421
|
+
acc[:global_write_census].merge(attribute_own_writes(census.write_census.census, path))
|
|
10422
|
+
end
|
|
10423
|
+
|
|
10424
|
+
# #1154 — tags each `[:own_write, class, ivar]` entry with the file that records it, so a reader of the merged
|
|
10425
|
+
# census can tell a second file writing the same ivar of the same class (`UnseenIvarWriters`).
|
|
10426
|
+
def attribute_own_writes(census, path)
|
|
10427
|
+
return census unless census.any? { |entry| entry.first == :own_write }
|
|
10428
|
+
|
|
10429
|
+
census.to_set { |entry| entry.first == :own_write ? [*entry, path].freeze : entry }
|
|
9592
10430
|
end
|
|
9593
10431
|
|
|
9594
10432
|
# Folds one file's Data + Struct member-layout tables into the cross-file accumulator (kept out of
|
|
@@ -9607,8 +10445,9 @@ module Rigor
|
|
|
9607
10445
|
def merge_class_keyed_index_tables(acc, root, file_methods, certainty)
|
|
9608
10446
|
file_visibilities, file_contested = build_method_visibility_tables(root, certainty: certainty)
|
|
9609
10447
|
siblings = acc[:siblings]
|
|
9610
|
-
siblings[:contested_discovered_method_visibilities] =
|
|
9611
|
-
siblings[:contested_discovered_method_visibilities], file_visibilities,
|
|
10448
|
+
siblings[:contested_discovered_method_visibilities] = fold_visibility_contests(
|
|
10449
|
+
siblings[:contested_discovered_method_visibilities], acc[:method_visibilities], file_visibilities,
|
|
10450
|
+
file_contested
|
|
9612
10451
|
)
|
|
9613
10452
|
file_visibilities.each do |class_name, table|
|
|
9614
10453
|
(acc[:method_visibilities][class_name] ||= {}).merge!(table)
|
|
@@ -9710,8 +10549,12 @@ module Rigor
|
|
|
9710
10549
|
singleton_cref: false)
|
|
9711
10550
|
return unless node.is_a?(Prism::Node)
|
|
9712
10551
|
|
|
10552
|
+
# #1154 — the class whose instance `def`s the ivar-writer census credits; none under `class << self`.
|
|
10553
|
+
ivar_owner = census_ivar_owner(qualified_prefix, self_owner, singleton_cref)
|
|
9713
10554
|
case node
|
|
9714
10555
|
when Prism::SingletonClassNode
|
|
10556
|
+
# #1154 — a `class << self` inside an instance `def` opens the instance's singleton class.
|
|
10557
|
+
tables.write_census.visit(node, top_level: qualified_prefix.empty?, owner: ivar_owner)
|
|
9715
10558
|
return walk_census_singleton_class(node, qualified_prefix, tables, self_owner,
|
|
9716
10559
|
meta_owner, singleton_cref)
|
|
9717
10560
|
when Prism::ClassNode, Prism::ModuleNode
|
|
@@ -9719,7 +10562,7 @@ module Rigor
|
|
|
9719
10562
|
singleton_cref)
|
|
9720
10563
|
else
|
|
9721
10564
|
census_constant_write(node, qualified_prefix, tables, self_owner, singleton_cref: singleton_cref)
|
|
9722
|
-
tables.write_census.visit(node, top_level: qualified_prefix.empty
|
|
10565
|
+
tables.write_census.visit(node, top_level: qualified_prefix.empty?, owner: ivar_owner)
|
|
9723
10566
|
end
|
|
9724
10567
|
|
|
9725
10568
|
rebound = rebound_block_self(node, qualified_prefix, nil, meta_owner, self_owner)
|
|
@@ -9751,6 +10594,16 @@ module Rigor
|
|
|
9751
10594
|
end
|
|
9752
10595
|
end
|
|
9753
10596
|
|
|
10597
|
+
# #1154 — the class whose instance `def`s the ivar-writer census credits: the class a `Struct.new` /
|
|
10598
|
+
# `Data.define` / `Class.new` constant or a `K.class_eval` block opens (`self_owner`), else the lexical class;
|
|
10599
|
+
# none under `class << self` or an opaque block. The prefix Array is joined only when an entry is recorded.
|
|
10600
|
+
def census_ivar_owner(qualified_prefix, self_owner, singleton_cref)
|
|
10601
|
+
return self_owner if self_owner.is_a?(String)
|
|
10602
|
+
return nil if self_owner || singleton_cref || qualified_prefix.empty?
|
|
10603
|
+
|
|
10604
|
+
qualified_prefix
|
|
10605
|
+
end
|
|
10606
|
+
|
|
9754
10607
|
# Under an unnameable cref a bare/`self::` header pushes `#<singleton>::Name` — still
|
|
9755
10608
|
# unnameable — so the body keeps the OPAQUE self and the cref flag; nameable
|
|
9756
10609
|
# headers re-anchor at a real cref.
|
|
@@ -10838,6 +11691,7 @@ module Rigor
|
|
|
10838
11691
|
table[node] = parent_scope
|
|
10839
11692
|
parent_scope
|
|
10840
11693
|
end
|
|
11694
|
+
evaluate_unentered_flow(node, table, current_scope)
|
|
10841
11695
|
|
|
10842
11696
|
case node
|
|
10843
11697
|
when Prism::IfNode
|
|
@@ -10853,7 +11707,7 @@ module Rigor
|
|
|
10853
11707
|
child_scope = entered ? current_scope : closure_scope(node, current_scope)
|
|
10854
11708
|
node.rigor_each_child { |child| propagate(child, table, child_scope) }
|
|
10855
11709
|
when Prism::CallNode
|
|
10856
|
-
propagate_call(node, table, current_scope)
|
|
11710
|
+
propagate_call(node, table, current_scope, recorded: recorded)
|
|
10857
11711
|
when Prism::RescueNode, Prism::EnsureNode
|
|
10858
11712
|
# Issue #1360 — a clause the evaluator did not enter (a `begin` in a value position) reads `$!`, `$@` and
|
|
10859
11713
|
# `$?` unbound, not the enclosing clause's; one it entered keeps what it recorded.
|
|
@@ -10867,6 +11721,84 @@ module Rigor
|
|
|
10867
11721
|
end
|
|
10868
11722
|
end
|
|
10869
11723
|
|
|
11724
|
+
# Issues #2196 and #2056 — the constructs whose parts run under a narrowing their own condition or left operand
|
|
11725
|
+
# makes, keyed to the part {StatementEvaluator} records first when it enters one. Issue #2214 — a `&&=` on a
|
|
11726
|
+
# variable runs its value under the variable's truthy narrowing (`[c &&= c + 1]`).
|
|
11727
|
+
UNENTERED_FLOW_ENTRY = {
|
|
11728
|
+
Prism::AndNode => :left, Prism::OrNode => :left,
|
|
11729
|
+
Prism::IfNode => :predicate, Prism::UnlessNode => :predicate,
|
|
11730
|
+
Prism::WhileNode => :predicate, Prism::UntilNode => :predicate,
|
|
11731
|
+
Prism::LocalVariableAndWriteNode => :value, Prism::InstanceVariableAndWriteNode => :value,
|
|
11732
|
+
Prism::ClassVariableAndWriteNode => :value, Prism::GlobalVariableAndWriteNode => :value,
|
|
11733
|
+
Prism::CaseNode => :predicate
|
|
11734
|
+
}.freeze
|
|
11735
|
+
private_constant :UNENTERED_FLOW_ENTRY
|
|
11736
|
+
|
|
11737
|
+
UNENTERED_EVALUATOR_KEY = :rigor_scope_index_unentered_evaluator
|
|
11738
|
+
private_constant :UNENTERED_EVALUATOR_KEY
|
|
11739
|
+
|
|
11740
|
+
# Issues #2196 and #2056 — an `&&` / `||` / `and` / `or`, an `if` / `unless` / ternary or a `while` / `until`
|
|
11741
|
+
# in a value position (an array element, a hash value, a call argument, an interpolation, a multi-value
|
|
11742
|
+
# `return`) is typed by `ExpressionTyper` and never entered, so no part of it was recorded and this walk handed
|
|
11743
|
+
# every part one scope. A walk that narrows by the condition's edges cannot follow a write inside the narrowed
|
|
11744
|
+
# part (`[c && (c = c.size; c.even?)]`, `[x || (set!; @x.upcase)]`) nor one inside a block in an earlier
|
|
11745
|
+
# conjunct (`[x.nil? && [1].each { x = "d" } && x.upcase]`). Such a construct is evaluated here instead, from
|
|
11746
|
+
# the scope this walk reached it with, under the index's own recorder, so each part is recorded with the scope
|
|
11747
|
+
# the statement form records it with. What it does not visit (a dead arm) is walked as before.
|
|
11748
|
+
def evaluate_unentered_flow(node, table, current_scope)
|
|
11749
|
+
entry = UNENTERED_FLOW_ENTRY[node.class]
|
|
11750
|
+
return if entry.nil?
|
|
11751
|
+
|
|
11752
|
+
# An entered construct is already recorded; skipping it is a performance guard, not a behaviour. A `case`
|
|
11753
|
+
# without a subject records its first `when` test first.
|
|
11754
|
+
part = node.public_send(entry) || (node.conditions.first&.conditions&.first if node.is_a?(Prism::CaseNode))
|
|
11755
|
+
return if part.nil? || table.key?(part)
|
|
11756
|
+
|
|
11757
|
+
evaluate = Thread.current[UNENTERED_EVALUATOR_KEY]
|
|
11758
|
+
evaluate&.call(node, current_scope, code_body_nodes(node, current_scope.source_path))
|
|
11759
|
+
end
|
|
11760
|
+
|
|
11761
|
+
# The evaluation of an unentered construct starts with no class frame and no lexical nesting, which only a code
|
|
11762
|
+
# body observes: a `def`, a `class` / `module` / `class << …` body, and the block of a class-creating call
|
|
11763
|
+
# (`Class.new { … }`, `Struct.new`, `Data.define`, `Module.new`) or of a `refine`, whose `def`s and constants
|
|
11764
|
+
# resolve against the class the walk tracks and the evaluation does not. Every node in one is left out of the
|
|
11765
|
+
# recording, so the walk reaches it unrecorded and handles it as it did before #2196 ({#unentered_block_entry},
|
|
11766
|
+
# {#record_class_body_defs}).
|
|
11767
|
+
# Answers nil, allocating nothing, for a construct holding no code body: the common case.
|
|
11768
|
+
def code_body_nodes(node, source_path, acc = nil, inside: false)
|
|
11769
|
+
inside ||= CODE_BODY_NODES.include?(node.class)
|
|
11770
|
+
(acc ||= Set.new.compare_by_identity) << node if inside
|
|
11771
|
+
if !inside && node.is_a?(Prism::CallNode) && class_body_block_call?(node, source_path)
|
|
11772
|
+
acc = code_body_nodes(node.block, source_path, acc, inside: true)
|
|
11773
|
+
node.rigor_each_child do |child|
|
|
11774
|
+
acc = code_body_nodes(child, source_path, acc) unless child.equal?(node.block)
|
|
11775
|
+
end
|
|
11776
|
+
return acc
|
|
11777
|
+
end
|
|
11778
|
+
node.rigor_each_child { |child| acc = code_body_nodes(child, source_path, acc, inside: inside) }
|
|
11779
|
+
acc
|
|
11780
|
+
end
|
|
11781
|
+
|
|
11782
|
+
CODE_BODY_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
11783
|
+
private_constant :CODE_BODY_NODES
|
|
11784
|
+
|
|
11785
|
+
def class_body_block_call?(node, source_path)
|
|
11786
|
+
node.block.is_a?(Prism::BlockNode) &&
|
|
11787
|
+
(!AnonymousMetaClass.name_for(node, source_path).nil? || !refine_target(node).nil?)
|
|
11788
|
+
end
|
|
11789
|
+
|
|
11790
|
+
# The evaluation {#evaluate_unentered_flow} runs for the walk of one {#index}, installed for that walk only.
|
|
11791
|
+
# Index walks nest (a rule may re-index a file while another file's walk is suspended), so the outer one is
|
|
11792
|
+
# restored on exit. A `propagate` called directly while an outer index walk is running would record into the
|
|
11793
|
+
# OUTER index's table through it; nothing outside {#index} calls `propagate`.
|
|
11794
|
+
def with_unentered_evaluator(evaluate)
|
|
11795
|
+
outer = Thread.current[UNENTERED_EVALUATOR_KEY]
|
|
11796
|
+
Thread.current[UNENTERED_EVALUATOR_KEY] = evaluate
|
|
11797
|
+
yield
|
|
11798
|
+
ensure
|
|
11799
|
+
Thread.current[UNENTERED_EVALUATOR_KEY] = outer
|
|
11800
|
+
end
|
|
11801
|
+
|
|
10870
11802
|
SELF_OPENING_BODIES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
10871
11803
|
private_constant :SELF_OPENING_BODIES
|
|
10872
11804
|
|
|
@@ -10901,11 +11833,11 @@ module Rigor
|
|
|
10901
11833
|
# argument one scope and threads none, so arguments that write keep the call's: an earlier one may rebind the
|
|
10902
11834
|
# receiver a later one reads (`return y&.concat((y = nil).to_s, y.upcase)`, where the evaluator threads no
|
|
10903
11835
|
# operand).
|
|
10904
|
-
def propagate_call(node, table, current_scope)
|
|
11836
|
+
def propagate_call(node, table, current_scope, recorded: false)
|
|
10905
11837
|
block = node.block
|
|
10906
11838
|
operands =
|
|
10907
11839
|
OperandEffects.any?(node.arguments) ? current_scope : Narrowing.safe_navigation_scope(node, current_scope)
|
|
10908
|
-
entry = unentered_block_entry(node, block, table, current_scope)
|
|
11840
|
+
entry = unentered_block_entry(node, block, table, current_scope, call_recorded: recorded)
|
|
10909
11841
|
if entry.equal?(current_scope) && operands.equal?(current_scope)
|
|
10910
11842
|
node.rigor_each_child { |child| propagate(child, table, current_scope) }
|
|
10911
11843
|
return
|
|
@@ -10929,7 +11861,7 @@ module Rigor
|
|
|
10929
11861
|
node.rigor_each_child { |child| propagate(child, table, child_scope) }
|
|
10930
11862
|
end
|
|
10931
11863
|
|
|
10932
|
-
def unentered_block_entry(node, block, table, current_scope)
|
|
11864
|
+
def unentered_block_entry(node, block, table, current_scope, call_recorded: false)
|
|
10933
11865
|
return current_scope unless block.is_a?(Prism::BlockNode) && !table.key?(block)
|
|
10934
11866
|
|
|
10935
11867
|
narrowing = unentered_block_narrowing(node, current_scope)
|
|
@@ -10942,6 +11874,12 @@ module Rigor
|
|
|
10942
11874
|
else
|
|
10943
11875
|
LastLine.block_entry(FreshFrameBlocks.closure_entry(current_scope, block, node), block, node)
|
|
10944
11876
|
end
|
|
11877
|
+
# Issue #2196 — a class-body block whose call the evaluator recorded but did not enter (one inside a construct
|
|
11878
|
+
# {#evaluate_unentered_flow} evaluated, or a later operand) starts from the scope the call was evaluated in,
|
|
11879
|
+
# which may narrow a captured local the body writes; this walk runs the body's statements from one scope, so
|
|
11880
|
+
# such a local reads `Dynamic[top]` in it: `[c && Class.new { c = c.size; c.even? }]`. A block whose call was
|
|
11881
|
+
# not recorded keeps the enclosing scope as before.
|
|
11882
|
+
entry = widen_condition_writes(block, entry) if call_recorded && class_body_block_call?(node, entry.source_path)
|
|
10945
11883
|
anonymous = AnonymousMetaClass.name_for(node, current_scope.source_path)
|
|
10946
11884
|
return unentered_meta_class_body_entry(block, entry, anonymous, table) if anonymous
|
|
10947
11885
|
|
|
@@ -11073,20 +12011,28 @@ module Rigor
|
|
|
11073
12011
|
scope.local(name).nil? ? scope : scope.with_local(name, Type::Combinator.untyped)
|
|
11074
12012
|
end
|
|
11075
12013
|
|
|
12014
|
+
# Issue #1924 — an `if` / `elsif` / `unless` / ternary the evaluator did not enter (`[if (x ||= "d").empty?
|
|
12015
|
+
# then 0 else x.upcase end]`) applies no write its condition makes, so its edges are taken from a scope where
|
|
12016
|
+
# every bound local the condition writes reads `Dynamic[top]` ({#widen_condition_writes}), as
|
|
12017
|
+
# {#propagate_subjectless_case} takes a `when` condition's. The condition itself reads the scope before it.
|
|
11076
12018
|
def propagate_if_branches(node, table, current_scope)
|
|
11077
|
-
truthy_scope, falsey_scope =
|
|
12019
|
+
truthy_scope, falsey_scope = conditional_edges(node.predicate, current_scope)
|
|
11078
12020
|
propagate(node.predicate, table, current_scope) if node.predicate
|
|
11079
12021
|
propagate(node.statements, table, truthy_scope) if node.statements
|
|
11080
12022
|
propagate(node.subsequent, table, falsey_scope) if node.subsequent
|
|
11081
12023
|
end
|
|
11082
12024
|
|
|
11083
12025
|
def propagate_unless_branches(node, table, current_scope)
|
|
11084
|
-
truthy_scope, falsey_scope =
|
|
12026
|
+
truthy_scope, falsey_scope = conditional_edges(node.predicate, current_scope)
|
|
11085
12027
|
propagate(node.predicate, table, current_scope) if node.predicate
|
|
11086
12028
|
propagate(node.statements, table, falsey_scope) if node.statements
|
|
11087
12029
|
propagate(node.else_clause, table, truthy_scope) if node.else_clause
|
|
11088
12030
|
end
|
|
11089
12031
|
|
|
12032
|
+
def conditional_edges(predicate, scope)
|
|
12033
|
+
Narrowing.predicate_scopes(predicate, widen_condition_writes(predicate, scope))
|
|
12034
|
+
end
|
|
12035
|
+
|
|
11090
12036
|
# Issue #1495 — a `case` without a subject the evaluator did not enter (`[case; when x.nil? then 0; else x.size
|
|
11091
12037
|
# end]`) narrows by its conditions' edges, as {StatementEvaluator#subjectless_when_scopes} does: each condition
|
|
11092
12038
|
# under the falsey edges of the conditions before it, each clause under the join of its conditions' truthy edges,
|
|
@@ -11120,7 +12066,8 @@ module Rigor
|
|
|
11120
12066
|
# A test that calls a method may rebind an instance-variable or global subject before the body reads it
|
|
11121
12067
|
# (`when String then 1 when reset! then @x.upcase`, where `reset!` writes `@x`), and the walk cannot tell. From
|
|
11122
12068
|
# the first such test on, the subject reads `Dynamic[top]` on both edges ({#forget_called_subject}). The
|
|
11123
|
-
# evaluator
|
|
12069
|
+
# evaluator forgets a global subject there too (issue #2211). A `case` / `when` the evaluator did not enter is
|
|
12070
|
+
# evaluated ({#evaluate_unentered_flow}); this walk handles only a clause that evaluation does not visit.
|
|
11124
12071
|
def propagate_subject_case(node, table, current_scope)
|
|
11125
12072
|
subject = node.predicate
|
|
11126
12073
|
propagate(subject, table, current_scope)
|